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を一から作成する その125 現在の局面の状況と合法手を着手した際の局面の状況の表示

Last updated at Posted at 2024-10-17

目次と前回の記事

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

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

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

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

Marubatsu_GUI クラスの改良の続き

前回の記事で、Marubatsu_GUI クラスの改良を行いました。今回の記事ではその続きで、下記の改良のうちの最初の 2 つを実装します。

  • 現在の局面の状況がわかるようにする
  • ゲーム盤のマスに、そのマスに着手を行った場合の局面の状況を表示する
  • ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できるようにする

現在の局面の状況がわかるようにする改良

現在の局面の状況 は、下部の GUI の部分木 の赤枠の選択された 局面の色を見ればわかる ようになっていますが、ゲーム盤にもその情報が表示されると便利 です。

ただし、局面の状況を常に表示してしまうと、真剣勝負の邪魔になる ので、ボタンでその表示を切り替える ことができるようにします。

現在の局面の状況の表示方法の検討

まず、局面の状況をどのように表示するかを決める必要があります。本記事では、局面の状況を表す下記表の 1 文字を表示 するという、シンプルな表示を行うことにします。別の方法で表示を行いたい人は自由に変更して下さい。

局面の状況 表示する文字
〇 の必勝の局面
引き分けの局面
× の必勝の局面 ×

また、局面の状況を表す文字は、ゲーム盤の上の 手番の右に表示 することにします。別の場所に表示したい人は自由に変更して下さい。

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

局面の状況の表示を行うようにするための 修正の流れ は、前回の記事で Marubatsu_GUI クラスの改良を行った場合のものと ほぼ同じ です。

まず、下記のプログラムのように、最初に局面の状況(status)を表示するかどうかを表す 仮引数 show_status を Marubatsu_GUI クラスの __init__ メソッドに追加 します。

また、局面の状況を計算して表示するため には、局面と最善手・評価値の対応表のデータが必要 なので、13 行目でそのデータをファイルから読み込んで score_table 属性に代入しています。

  • 6 行目:仮引数 show_status を追加する
  • 8 行目show_status を同名の属性に代入する
  • 12、14 行目:局面と最善手・評価値の対応表のデータをファイルから読み込んで score_table 属性に代入する。なお、12 行目で load_bestmovesローカルなインポートを行っているのは、循環参照を防ぐためである
 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, show_subtree,
 6               show_status, seed, size):
元と同じなので省略
 7      self.show_subtree = show_subtree
 8      self.show_status = show_status
 9      self.seed = seed
10      self.size = size
11    
12      from util import load_bestmoves
13
14      self.score_table = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
元と同じなので省略
15    
16  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, show_subtree,
             show_status, 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.show_subtree = show_subtree
    self.show_status = show_status
    self.seed = seed
    self.size = size

    from util import load_bestmoves

    self.score_table = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
    
    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, scoretable_dict, show_subtree,
-            seed, size):
+            show_status, seed, size):
元と同じなので省略
    self.show_subtree = show_subtree
+   self.show_status = show_status
    self.seed = seed
    self.size = size

+   from util import load_bestmoves
   
+   self.score_table = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
元と同じなので省略
    
Marubatsu_GUI.__init__ = __init__

これまでの記事で、局面と最善手・評価値の対応表のデータは 通常の方法で評価値を計算したもの と、最短の勝利を優先して評価値を計算したもの の 2 つを作成しましたが、どちらも 〇 の必勝の局面の評価値を正の値、引き分けの局面の評価値を 0、× の必勝の曲年の評価値を負の値 として計算している ので、局面の状況を調べて表示する場合は どちらの対応表のデータを読み込んでもかまいません

また、局面と最善手・評価値の対応表のデータは Mbtree_GUI クラスでもファイルから読みんでいますが、ファイルサイズが小さく、ほぼ一瞬で読み込むことができる ので 重複して読み込む ことにしました。

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

次に、Marubatsu_GUI クラスのインスタンスを作成する Marubatsu クラスの play メソッドを以下のプログラムのように修正します。

  • 4 行目:デフォルト値を False1 とする仮引数 show_status を追加する
  • 6 行目:Marubatsu_GUI クラスのインスタンスを作成する際に、実引数 show_status=show_status を追加する
1  from marubatsu import Marubatsu
2
3  def play(self, ai:list, ai_dict=None, params=None, names=None, scoretable_dict=None,
4           show_subtree=True, show_status=False, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
5      if gui:
6          mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict, 
7                                 show_subtree=show_subtree, show_status=show_status, seed=seed, size=size)
元と同じなので省略                        
8
9  Marubatsu.play = play
行番号のないプログラム
from marubatsu import Marubatsu

def play(self, ai:list, ai_dict=None, params=None, names=None, scoretable_dict=None,
         show_subtree=True, show_status=False, 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, 
                              show_subtree=show_subtree, show_status=show_status, seed=seed, size=size)
    else:
        mb_gui = None
        
    self.restart()
    return self.play_loop(mb_gui, params=params)

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

def play(self, ai:list, ai_dict=None, params=None, names=None, scoretable_dict=None,
-        show_subtree=True, verbose=True, seed=None, gui=False, size=3):
+        show_subtree=True, show_status=False, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    if gui:
        mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict, 
-                              show_subtree=show_subtree, seed=seed, size=size)
+                              show_subtree=show_subtree, show_status=show_status, seed=seed, size=size)
元と同じなので省略                        

Marubatsu.play = play

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

2024/11/16 修正
create_widgets の最後に余計な処理が記述されていたので修正しました。

次に、局面の状況の 表示の有無を選択するボタンを作成 する必要があります。本記事では、ボタンに表示する文字 を「状況」としました。下記は、そのように create_widgets を修正したプログラムです。

  • 5 行目:「状況」と表示するボタンを作成する
1  import ipywidgets as widgets 
2
3  def create_widgets(self):
元と同じなので省略
4      # 状況ボタン、大きさを変更する FloatSlider を作成する        
5      self.show_status_button = self.create_button("状況", 50)
6      self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
7                                          description="size", value=self.size)
元と同じなので省略
8    
9  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)

    # 状況ボタン、大きさを変更する FloatSlider を作成する        
    self.show_status_button = self.create_button("状況", 50)
    self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
                                        description="size", value=self.size)
    
    # 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"    

    self.output = widgets.Output()  
    self.print_helpmessage()
    self.output.layout.display = "none"
    
Marubatsu_GUI.create_widgets = create_widgets
修正箇所
import ipywidgets as widgets 

def create_widgets(self):
元と同じなので省略
    # 状況ボタン、大きさを変更する FloatSlider を作成する        
+   self.show_status_button = self.create_button("状況", 50)
    self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
                                        description="size", value=self.size)
元と同じなので省略
    
Marubatsu_GUI.create_widgets = create_widgets

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

「状況」ボタンは、FloatSlider の左に配置 することにします。下記はそのように display_widgets を修正したプログラムです。

  • 3 行目:FloatSlider の左に「状況」ボタンのウィジェットを配置するようにする
1  def display_widgets(self):
元と同じなので省略
2      # 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
3      hbox2 = widgets.HBox([self.show_status_button, self.size_slider])
元と同じなので省略
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])
    # 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
    hbox2 = widgets.HBox([self.show_status_button, self.size_slider])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help])) 
    
Marubatsu_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
元と同じなので省略
    # 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
-   hbox2 = widgets.HBox([self.size_slider])
+   hbox2 = widgets.HBox([self.show_status_button, self.size_slider])
元と同じなので省略
    
Marubatsu_GUI.display_widgets = display_widgets

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

次に、create_event_handler 以下のプログラムのように修正します。

  • 4 ~ 6 行目:「状況」ボタンをクリックした際に実行されるイベントハンドラを定義する。行う処理は、show_status 属性の値を反転し、画面の表示を更新する処理である
  • 14 行目:「状況」ボタンとイベントハンドラを結び付ける
 1  import math
 2
 3  def create_event_handler(self):
元と同じなので省略
 4      def on_show_status_button_clicked(b=None):
 5          self.show_status = not self.show_status
 6          self.update_gui()
 7
 8      def on_size_slider_changed(changed):
 9          self.size = changed["new"]
10          self.fig.set_figwidth(self.size)
11          self.fig.set_figheight(self.size)
12          self.update_gui()
13
14      self.show_status_button.on_click(on_show_status_button_clicked)
15      self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
    
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.show_subtree = not self.show_subtree
        self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
        self.update_gui()
        
    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_show_status_button_clicked(b=None):
        self.show_status = not self.show_status
        self.update_gui()

    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.size)
        self.fig.set_figheight(self.size)
        self.update_gui()

    self.show_status_button.on_click(on_show_status_button_clicked)
    self.size_slider.observe(on_size_slider_changed, names="value")

    # 変更ボタンのイベントハンドラを定義する
    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_show_status_button_clicked(b=None):
+       self.show_status = not self.show_status
+       self.update_gui()

    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.size)
        self.fig.set_figheight(self.size)
        self.update_gui()

+   self.show_status_button.on_click(on_show_status_button_clicked)
    self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler

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

次に、局面の状況を表示 する処理を update_gui に記述します。決着がついている場合は o win のような表示が行われるので、局面の状況は 決着がついていない場合 に行えばよいでしょう。下記は、そのように update_gui を修正したプログラムです。

  • 3、5 行目:ゲームの決着がついておらず、局面の状況を表示するかどうかを表す self.show_statusTrue であることを判定する
  • 6 行目score_table 属性に代入された局面と最善手・評価値の対応表のデータと、局面を表す文字列を利用して、現在の局面の評価値を計算する
  • 7 行目:手番を表す文字列と局面の状況を表す文字列の 間を空ける ために、半角の空白を text の後に追加 する
  • 8 ~ 13 行目:評価値の値に応じて、手番を表す text の後に局面の状況を表す文字列を計算して追加する
 1  def update_gui(self):
元と同じなので省略
 2      # ゲームの決着がついていない場合は、手番を表示する
 3      if self.mb.status == Marubatsu.PLAYING:
 4          text = "Turn " + self.mb.turn
 5          if self.show_status:
 6              score = self.score_table[self.mb.board_to_str()]["score"]
 7              text += " "
 8              if score > 0:
 9                  text += ""
10              elif score == 0:
11                  text += ""
12              else:
13                  text += "×"
元と同じなので省略
14       
15  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=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        if self.show_status:
            score = self.score_table[self.mb.board_to_str()]["score"]
            text += " "
            if score > 0:
                text += ""
            elif score == 0:
                text += ""
            else:
                text += "×"
    # 引き分けの場合
    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=7*self.size)

    self.draw_board(ax, self.mb, lw=0.7*self.size)

    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):
元と同じなので省略
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
+       if self.show_status:
+           score = self.score_table[self.mb.board_to_str()]["score"]
+           text += " "
+           if score > 0:
+               text += ""
+           elif score == 0:
+               text += ""
+           else:
+               text += "×"
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui

Marubastu_GUI クラスの update_widgets_status メソッドの修正

最後に、show_status 属性の値に応じて 「状況」ボタンの表示の色を変更する ために、下記のプログラムのように update_widgets_status の 5 行目を追加します。

1  def update_widgets_status(self):
2      self.inttext.disabled = not self.checkbox.value
3      self.set_button_color(self.show_tree_button, self.show_subtree)    
4      self.set_button_status(self.reset_tree_button, not self.show_subtree)    
5      self.set_button_color(self.show_status_button, self.show_status)    
元と同じなので省略
6    
7  Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
def update_widgets_status(self):
    self.inttext.disabled = not self.checkbox.value
    self.set_button_color(self.show_tree_button, self.show_subtree)    
    self.set_button_status(self.reset_tree_button, not self.show_subtree)    
    self.set_button_color(self.show_status_button, self.show_status)    
    self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
    self.set_button_status(self.prev_button, self.mb.move_count <= 0)
    self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
    self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)    
    # value 属性よりも先に max 属性に値を代入する必要がある点に注意!
    self.slider.max = len(self.mb.records) - 1
    self.slider.value = self.mb.move_count
    
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
def update_widgets_status(self):
    self.inttext.disabled = not self.checkbox.value
    self.set_button_color(self.show_tree_button, self.show_subtree)    
    self.set_button_status(self.reset_tree_button, not self.show_subtree)    
+   self.set_button_color(self.show_status_button, self.show_status)    
元と同じなので省略
    
Marubatsu_GUI.update_widgets_status = update_widgets_status

上記の修正後に下記のプログラムを実行すると、初期設定では「状況」ボタンが OFF になっているので実行結果の左図のように局面の状況は表示されません。

次に「状況」ボタンをクリックすると、実行結果の真ん中の図のように手番を表す文字の右に 局面の状況を表す文字列が表示される ようになります。実行結果の右図は 〇 の必勝の局面の場合 の図です。表示がわかりづらいと思った方は、自由に変更して下さい。

from util import gui_play

gui_play()

   

着手を行った場合の局面の状況の表示

現状では、それぞれの合法手に 着手を行った局面の状況 は、下部の GUI の部分木の選択された局面の 子ノードの局面の色を見ればわかる ようになっていますが、直観的に わかりやすいとは言えない でしょう。そこで、ゲーム盤のマス に着手を行った場合の 局面の状況を直接表示 するという改良を行うことにします。

本記事では、その 表示の切り替え を、先程の 「状況」ボタンで行う ことにします。「状況」ボタンとは別のボタンを配置して表示の切り替えを別々に行いたい人は、そのように実装してみて下さい。

着手を行った場合の局面の状況の表示方法の検討

まず、ゲーム盤のマスに 着手を行った場合の局面の状況どのように表示するかを決める 必要があります。本記事ではゲーム盤の マスの中の左上 に、そのマスに 着手した場合の局面の状況 を、先程と同様の 〇、△、× の一文字を 小さく表示する ことにします。左上に小さく表示するのは、マスに 配置されたマークと区別できるようにするため です。

また、別の工夫として 最善手であるかどうかを区別できる ように、最善手の場合文字を赤く表示 することにします。その際に、最短の勝利を優先 して計算された局面と最善手・評価値の対応表を利用すると、一部の最善手が赤く表示されなくなる ので、局面と最善手・評価値の対応表は、先程のプログラムで score_table 属性に代入した bestmoves_and_score_by_board.dat のファイルのデータを利用することにします。

着手を行った場合の局面の状況を表示する処理の記述場所

新しいボタンを配置しないことにしたので、__init__ メソッドや create_widgets メソッド などの修正を行う必要はありません。修正する必要があるのは ゲーム盤の表示を行う処理 で、その処理は update_gui メソッドの下記のプログラムで行っています。

def update_gui(self):

        self.draw_board(ax, self.mb, lw=0.7*self.size)

着手を行った場合の局面の状況を 表示する処理 は、以下の 2 種類の方法で実装することができます。

  • draw_board メソッドの中に記述する
  • update_gui メソッドの中で、上記の draw_board メソッドを呼び出した後に記述する

どちらの方法で実装しても構いませんが、draw_board メソッドの中に記述すると、draw_board に仮引数を追加するなどの修正が少々面倒なので、本記事では後者の方法で実装することにします。

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

着手を行った場合の局面の状況を表示する処理は、下記の手順行います。

show_status 属性が True の場合に それぞれの合法手 に対して下記の処理を行う。

  1. 合法手を着手した場合の 局面の状況を計算 する
  2. 文字の色 を合法手が最善手の場合は赤、そうでない場合は黒とする
  3. 合法手のマスの左上に、手順 1 で計算した局面の状況を、手順 2 で計算した文字の色で表示する

下記は、そのように update_gui メソッドを修正したプログラムです。

  • 6 行目show_status 属性が True の場合に処理を行うようにする
  • 7 行目:局面と最善手・評価値の対応表から、現在の局面の最善手の一覧を計算して bestmoves に代入する
  • 8 行目:現在の局面のそれぞれの合法手に対する繰り返しの処理を行う
  • 9、10 行目:現在の局面を表す self.mb に対する深いコピーを計算して mb に代入し、mb に対して合法手の着手を行う
  • 11 行目:局面と最善手・評価値の対応表から、合法手を着手した局面の評価値を計算する
  • 12 行目:合法手が最善手であるかどうかによって、表示する文字の色を計算する
  • 13 ~ 18 行目:評価値から局面の状況を表す文字列を計算する
  • 19 行目:合法手のマスの左上に、上記で計算した局面の状況を表す文字を表示する。表示する文字の 座標 と、文字の大きさ試行錯誤して決めた ものである。また、文字の大きさ前回の記事で説明したように、ゲーム盤の大きさを表す self.size に比例した値にする必要がある 点に注意する事
 1  from copy import deepcopy
 2
 3  def update_gui(self):
元と同じなので省略
 4      self.draw_board(ax, self.mb, lw=0.7*self.size)
 5   
 6      if self.show_status:
 7          bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
 8          for x, y in self.mb.calc_legal_moves():
 9              mb = deepcopy(self.mb)
10              mb.move(x, y)
11              score = self.score_table[mb.board_to_str()]["score"]
12              color = "red" if (x, y) in bestmoves else "black"
13              if score > 0:
14                  text = ""
15              elif score == 0:
16                  text = ""
17              else:
18                  text = "×"            
19              ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
元と同じなので省略
20        
21  Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
from copy import deepcopy

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=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
            text += " "
            if score > 0:
                text += ""
            elif score == 0:
                text += ""
            else:
                text += "×"
    # 引き分けの場合
    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=7*self.size)

    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        for x, y in self.mb.calc_legal_moves():
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if (x, y) in bestmoves else "black"
            if score > 0:
                text = ""
            elif score == 0:
                text = ""
            else:
                text = "×"            
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)

    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
修正箇所
from copy import deepcopy

def update_gui(self):
元と同じなので省略
    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
+   if self.show_status:
+       bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
+       for x, y in self.mb.calc_legal_moves():
+           mb = deepcopy(self.mb)
+           mb.move(x, y)
+           score = self.score_table[mb.board_to_str()]["score"]
+           color = "red" if (x, y) in bestmoves else "black"
+           if score > 0:
+               text = ""
+           elif score == 0:
+               text = ""
+           else:
+               text = "×"            
+           ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui

上記の修正後に下記のプログラムを実行し、「状況」ボタンをクリックすると、実行結果のように 空いているマス局面の状況を表す文字が表示される ようになります。

ゲーム開始時の局面はすべての合法手が最善手で、どのマスに着手を行っても引き分けの局面になるので、左図のようにすべてのマスに赤い △ が表示されます。

右図の (1, 1) に着手した局面は、四隅の合法手が最善手で引き分けの局面になるので赤い △ が、辺の合法手に着手すると 〇 の必勝の局面になるので黒い 〇 が表示されます。

他にも様々な着手を行い、下部の GUI の部分木の表示と比較して正しい表示が行われることを確認してみて下さい。

gui_play()

実行結果

 

update_gui メソッドの修正

上記のプログラムでは、update_gui の中で、評価値を表す score から局面の状況を表す文字列を計算するという、ほとんど同じ内容の処理2 箇所で記述している という問題点があるので、修正することにします。

具体的には、下記のような関数update_gui メソッドの中にローカル関数として定義 し、その関数を呼び出すようにします。なお、calc_status_txt をローカル関数として定義したのは、この関数を update_gui 以外で利用することがなさそうだったからです。他の場所から利用する可能性がある場合はメソッドとして定義したほうが良いでしょう。

名前:局面の状況を表す文字列を計算するので calc_status_txt とする
処理:評価値から局面の状況を表す文字列を計算する
入力:仮引数 score に評価値を代入する
出力:局面の状況を表す文字列を返り値として返す

下記は、そのように update_gui を修正したプログラムです。なお、10 行目の下に記述されていた ai = self.mb.ai は、ローカル変数 aiupdate_gui の中で使われていないことに気づいたので削除しました。

  • 2 ~ 8 行目:上記の calc_status_txtupdate_gui のローカル関数として定義する
  • 16、24 行目calc_status_txt を利用して局面の状況を表す文字列を計算するように修正する
 1  def update_gui(self):
 2      def calc_status_txt(score):
 3          if score > 0:
 4              return ""
 5          elif score == 0:
 6              return ""
 7          else:
 8              return "×"
 9   
10      ax = self.ax
元と同じなので省略
11      # ゲームの決着がついていない場合は、手番を表示する
12      if self.mb.status == Marubatsu.PLAYING:
13          text = "Turn " + self.mb.turn
14          score = self.score_table[self.mb.board_to_str()]["score"]
15          if self.show_status:
16              text += " " + calc_status_txt(score)
元と同じなので省略            
17      if self.show_status:
18          bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
19          for x, y in self.mb.calc_legal_moves():
20              mb = deepcopy(self.mb)
21              mb.move(x, y)
22              score = self.score_table[mb.board_to_str()]["score"]
23              color = "red" if (x, y) in bestmoves else "black"
24              text = calc_status_txt(score)
25              ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    def calc_status_txt(score):
        if score > 0:
            return ""
        elif score == 0:
            return ""
        else:
            return "×"
    
    ax = self.ax

    # 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=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
            text += " " + calc_status_txt(score)
    # 引き分けの場合
    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=7*self.size)

    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        for x, y in self.mb.calc_legal_moves():
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if (x, y) in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)

    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):
+   def calc_status_txt(score):
+       if score > 0:
+           return ""
+       elif score == 0:
+           return ""
+       else:
+           return "×"
    
    ax = self.ax
-   ai = self.mb.ai    
元と同じなので省略
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
-           text += " "
-           if score > 0:
-               text += ""
-           elif score == 0:
-               text += ""
-           else:
-               text += "×"
+           text += " " + calc_status_txt(score)
元と同じなので省略            
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        for x, y in self.mb.calc_legal_moves():
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if (x, y) in bestmoves else "black"
-           if score > 0:
-               text = ""
-           elif score == 0:
-               text = ""
-           else:
-               text = "×" 
+           text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui

実行結果は先ほどと同じなので省略しますが、下記のプログラムを実行して様々な操作を行った際に、正しい表示が行われることを確認して下さい。

gui_play()

今回の記事のまとめ

今回の記事では、「状況」ボタンを作成し、現在の局面の状況と、それぞれの合法手を着手した際の局面の状況を表示できるように修正しました。次回の記事では、現在の局面に対するそれぞれの AI が計算した最善手と評価値を表示する処理を実装します。

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

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

次回の記事

更新日時 更新内容
2024/11/16 create_widgets の最後に余計な処理が記述されていたので削除しました
  1. 本記事ではデフォルト値を False としましたが、True にしたい人は自由に変更して下さい。

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?