0
0

Pythonで〇×ゲームのAIを一から作成する その113 パラメータを必要とする AI に対する GUI の対戦機能の実装

Last updated at Posted at 2024-09-05

目次と前回の記事

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

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

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

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

GUI による対戦時の発生するエラーの検証と修正

前回までの記事で作成した 強解決の AI を GUI で対戦 しようとすると エラーが発生する場合があります。今回の記事ではそのエラーが発生する理由を検証して修正します。

play メソッドを利用した ai_gt6 どうしの対戦

強解決の AI である ai_gt61 どうしの対戦は、下記のプログラムのように Marubatsu クラスの play メソッドを利用することで行うことができます。なお、強解決の AI なので、実行結果のように、結果は必ず引き分けになります。

from marubatsu import Marubatsu
from util import load_bestmoves
from ai import ai_gt6

bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
param = {"bestmoves_by_board": bestmoves_by_board}
mb = Marubatsu()
mb.play(ai=[ai_gt6, ai_gt6], params=[param, param])

実行結果(実行結果はランダムなので下記と異なる場合があります)

Turn o
...
...
...

略

winner draw
oOx
xxo
oxo

また、下記のプログラムのように、同じ ai_gt6 どうしの対戦を play メソッドの実引数に gui=True を記述して GUI で対戦すると、実行結果のように対戦結果が表示されます。

mb.play(ai=[ai_gt6, ai_gt6], params=[param, param], gui=True)

実行結果(実行結果はランダムなので下記と異なる場合があります)

GUI での 人間 VS ai_gt6 の対戦時のエラー

上記のように、ai_gt6 どうしの対戦は CUI と GUI のどちらでも問題なく行うことができますが、下記のプログラムのように GUI で 人間 VS ai_gt6 の対戦を行った場合に 人間が着手を行うと 実行結果のように エラーが発生 してしまいます。なお、GUI のボタンやゲーム盤の画像は特におかしな点はないので省略します。

mb.play(ai=[None, ai_gt6], params=[{}, param], gui=True)

実行結果

略
File c:\Users\ys\ai\marubatsu\113\marubatsu.py:828, in Marubatsu_GUI.create_event_handler.<locals>.on_mouse_down(event)
    826     self.mb.move(x, y)                
    827 # 次の手番の処理を行うメソッドを呼び出す
--> 828 self.mb.play_loop(self)

File c:\Users\ys\ai\marubatsu\113\marubatsu.py:396, in Marubatsu.play_loop(self, mb_gui, params)
    394 # ai が着手を行うかどうかを判定する
    395 if ai[index] is not None:
--> 396     x, y = ai[index](self, **params[index])
    397 else:
    398     # キーボードからの座標の入力
    399     coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")

TypeError: ai_gt6() missing 1 required positional argument: 'bestmoves_by_board'

エラーの原因の検証

エラーメッセージから、play_loop 内の x, y = ai[index](self, **params[index]) によって ai_gt6 を呼び出した結果、ai_gt6 の仮引数である bestmoves_by_board に対応する位置引数(positional argument)が記述されていない ことがわかります。また、そのことから、params[index] に代入された dict に bestmoves_by_board というキーが存在していない ことがわかります。

しかし、play メソッドを下記のプログラムで呼び出した際には、下記のプログラムのように キーワード引数 params の要素には bestmoves_by_board というキーを持つ dict が代入されているので、上記のようなエラーが発生するのはおかしいと思えるかもしれません。

param = {"bestmoves_by_board": bestmoves_by_board}
mb.play(ai=[None, ai_gt6], params=[{}, param], gui=True)

そこで、params[index] に何が代入されているか をエラーメッセージをさかのぼって 検証する ことにします。一番下のエラーメッセージの少し上に記述されている下記のエラーメッセージの部分から、play_loop が Marubatsu_GUI クラスの create_event_handler 内で定義された on_mouse_down 内の self.mb.play_loop(self) で呼び出された ことがわかります。
なお、読みやすいようにエラーメッセージの途中で改行しました。

File c:\Users\ys\ai\marubatsu\113\marubatsu.py:828, in 
     Marubatsu_GUI.create_event_handler.<locals>.on_mouse_down(event)
    826     self.mb.move(x, y)                
    827 # 次の手番の処理を行うメソッドを呼び出す
--> 828 self.mb.play_loop(self)

エラーメッセージの 2 行目の create_event_handler.<locals>.on_mouse_down の <locals> は、その後ろに記述された on_mouse_down が、その前に記述された create_event_handler 内で定義された ローカル関数であることを表します

下記は、play_loop の定義の冒頭の部分 です。下記の定義から、self.mb.play_loop(self) によって play_loop が呼び出されると仮引数 mb_gui には self.mb.play_loop(self)self が、仮引数 params には対応する実引数が記述されていない ので None が代入され、2、3 行目の処理によって params には [{}, {}] が代入される ことになります。

1  def play_loop(self, mb_gui, params=None):
2      if params is None:
3          params = [{}, {}]

従って、index の値が 0 と 1 のどちらの場合も params[index] には空の dict である {} が代入され、エラーが発生する x, y = ai[index](self, **params[index]) では、確かに params[index]bestmoves_by_board というキーを持たない ことが確認できました。

このエラーは以前の記事で、play_loop メソッドに仮引数 params を追加するように修正した際 に、Marubatsu_GUI クラス内で play_loop メソッドを呼び出す処理を修正するのを後回しにしたことが原因 です。

そのことは、その時の記事で下記のように言及しています。

「なお、Marubatsu_GUI を利用して ai_gt1 で対戦を行う場合は Marubatsu_GUI 内で行う play_loop の呼び出しの処理の修正を行う必要があるが、その修正は今後の記事で行う」

Marubatsu_GUI クラス内で play_loop メソッドを呼び出しているのは、下記のプログラムの 6 行目と 16 行目 で、いずれも create_event_handler メソッド内で定義された 2 つの イベントハンドラの中に記述 されています。

 1  def create_event_handler(self):

 2      # 変更ボタンのイベントハンドラを定義する
 3      def on_change_button_clicked(b):
 4          for i in range(2):
 5              self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
 6          self.mb.play_loop(self)

 7      # ゲーム盤の上でマウスを押した場合のイベントハンドラ
 8      def on_mouse_down(event):
 9          # Axes の上でマウスを押していた場合のみ処理を行う
10          if event.inaxes and self.mb.status == Marubatsu.PLAYING:
11              x = math.floor(event.xdata)
12              y = math.floor(event.ydata)
13              with self.output:
14                  self.mb.move(x, y)                
15              # 次の手番の処理を行うメソッドを呼び出す
16              self.mb.play_loop(self)

それぞれのイベントハンドラは、GUI の「変更」ボタンをクリックして AI を変更した場合 と、ゲーム盤のマスをクリックして人間が着手を行った場合 に呼び出されるので、これらのイベントハンドラは AI どうしが対戦を行った場合は呼び出されることはありません。先ほど GUI で ai_gt6 どうしの対戦を行った際に エラーが発生しなかったのはそのためです

上記から、このエラーは パラメーターを必要とする AI と人間が GUI で対戦を行った際に発生する ことがわかります。強解決の AI である ai_gt1 ~ ai_gt6 はいずれもパラメーターを必要とするので、それらの AI と人間が GUI で対戦するとこのエラーが発生します。

Marubatsu_GUI と Marubatsu クラスの修正

このエラーを修正するためには、Marubatsu_GUI クラス内で play_loop メソッドを呼び出す際 に、AI の処理に必要な パラメーターを実引数に記述する 必要があります。

しかし、下記の Marubatsu_GUI クラスの __init__ メソッドには、AI の処理に必要な パラメーターの情報を代入する仮引数が存在しない ので、Marubatsu_GUI クラス内で AI の処理に必要な パラメーターを実引数に記述して play_loop を呼び出すことはできません

def __init__(self, mb, ai_dict=None, seed=None, size=3):

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

そこで、下記のプログラムのように、__init__ メソッドに AI の処理に必要なパラメーターの情報を代入する 仮引数 params を追加する ことにします。

  • 5 ~ 7 行目:デフォルト値を None とする仮引数 params を追加し、paramsNone の場合に params[{}, {}] を代入する
  • 8、9 行目:この処理は __init__ の後半に記述していたが、仮引数が None の場合の処理は先頭に記述するのが一般的なのでこちらに移動した
  • 12 行目:仮引数 params を同名の属性に代入することで、Marubatsu_GUI クラスの中で仮引数 params の値をどこでも利用できる ようになる
  • 16 行目:この行の修正については、この後で説明する
 1  from marubatsu import Marubatsu_GUI
 2  from tkinter import Tk
 3  import os
 4
 5  def __init__(self, mb, params=None, ai_dict=None, seed=None, size=3):
 6      if params is None:
 7          params = [{}, {}]
 8      if ai_dict is None:
 9          ai_dict = {}
元と同じなので省略    
10      self.mb = mb
11      self.ai_dict = ai_dict
12      self.params = params
13      self.seed = seed
14      self.size = size
15    
16      super(Marubatsu_GUI, self).__init__()
17    
18  Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os

def __init__(self, mb, params=None, ai_dict=None, seed=None, size=3):
    if params is None:
        params = [{}, {}]
    if ai_dict is None:
        ai_dict = {}
    
    # 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.seed = seed
    self.size = size
    
    super(Marubatsu_GUI, self).__init__()
    
Marubatsu_GUI.__init__ = __init__
修正箇所
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os

-def __init__(self, mb, ai_dict=None, seed=None, size=3):
+def __init__(self, mb, params=None, ai_dict=None, seed=None, size=3):
+   if params is None:
+       params = [{}, {}]
+   if ai_dict is None:
+       ai_dict = {}
元と同じなので省略    
    self.mb = mb
    self.ai_dict = ai_dict
+   self.params = params
    self.seed = seed
    self.size = size

-   # ai_dict が None の場合は、空の list で置き換える
-   if ai_dict is None:
-       self.ai_dict = {}
            
-   super().__init__()
+   super(Marubatsu_GUI, self).__init__()
    
Marubatsu_GUI.__init__ = __init__

super の修正に関する説明

以前の記事で、super は「クラスの定義の中で記述されたメソッドの中でしか利用できない」と説明しましたが、実は下記のように記述することで、クラスの定義の外 で記述されたメソッドの中でも 利用できる ことがわかりました。

super(クラス名, self)

従って __init__ メソッド内の super を下記のように修正することで、super が記述された __init__ メソッドを クラスの定義の外で定義して修正 することができます。

super(Marubatsu_GUI, self).__init__()

なお、super の実引数の意味は、クラスを複数のクラスから継承した際に重要となりますが、その説明はかなり複雑になので今回の記事では説明しません。また、Marubatsu_GUI クラスは GUI という 1 つのクラスしか継承していないので、現時点ではその意味を正確に理解する必要はありません。興味がある方は下記のリンク先を参照して下さい。

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

次に、下記のプログラムの 8、18 行目のように、create_event_handler 内で play_loop メソッドを呼び出す際に、仮引数 params に対応する実引数を記述する ように修正します。

 1  import math
 2
 3  def create_event_handler(self):
元と同じなので省略
 4      # 変更ボタンのイベントハンドラを定義する
 5      def on_change_button_clicked(b):
 6          for i in range(2):
 7              self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
 8          self.mb.play_loop(self, self.params)
元と同じなので省略
 9      # ゲーム盤の上でマウスを押した場合のイベントハンドラ
10      def on_mouse_down(event):
11          # Axes の上でマウスを押していた場合のみ処理を行う
12          if event.inaxes and self.mb.status == Marubatsu.PLAYING:
13              x = math.floor(event.xdata)
14              y = math.floor(event.ydata)
15              with self.output:
16                  self.mb.move(x, y)                
17              # 次の手番の処理を行うメソッドを呼び出す
18              self.mb.play_loop(self, self.params)
元と同じなので省略    
19    
20  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"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value               
                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):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[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,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

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

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

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_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] = None if self.dropdown_list[i].value == "人間" else 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_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
-       self.mb.play_loop(self)
+       self.mb.play_loop(self, self.params)
元と同じなので省略
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    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.mb.play_loop(self, self.params)
元と同じなので省略    
    
Marubatsu_GUI.create_event_handler = create_event_handler

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

次に、下記のプログラムの 4 行目のように、Marubatsu クラスの play メソッド内で Marubatsu_GUI クラスのインスタンスを作成する際に 仮引数 params に対応する実引数を記述する ように修正します。

1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3      if gui:
4          mb_gui = Marubatsu_GUI(self, params=params, ai_dict=ai_dict, seed=seed, size=size)  
5      else:
6          mb_gui = None
7
8  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=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, ai_dict=ai_dict, seed=seed, size=size)  
    else:
        mb_gui = None
        
    self.restart()
    return self.play_loop(mb_gui, params=params)

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

Marubatsu.play = play

実行結果は省略しますが、下記のプログラムを実行し、ゲーム盤のマスをクリックして着手を行っても エラーが発生しなくなったことを確認 して下さい。また、最後まで ai_gt6 と対戦を行うことができることも確認して下さい。

mb.play(ai=[None, ai_gt6], params=[{}, param], gui=True)

こちらも実行結果は省略しますが、念のため下記のプログラムで手番を入れ替えた場合でもプログラムが正しく動作する事を確認して下さい。

mb.play(ai=[ai_gt6, None], params=[param, {}], gui=True)

gui_play の修正

play メソッドによって、GUI で ai_gt6 の対戦を行うことができるようになったので、次は gui_playai_gt6 を選択できるように修正 することにします。

下記は現状の gui_play の定義です。このプログラムを見て、どのように修正すれば良いかを少し考えてみて下さい。

def gui_play(ai=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": "人間"}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = getattr(ai_module, ai_name)
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_dict=ai_dict, seed=seed, gui=True)

仮引数 params の追加

gui_play は仮引数 ai で、実行時の対戦カードを指定できるようになっていますが、AI のパラメーターを代入する仮引数が存在しない ので、このままは ai_gt6 のようなパラメーターを必要とする AI の対戦を最初に行うことはできません。そこで、下記のプログラムのように 仮引数 params を追加する必要 があります。

  • 1 行目gui_play には ai という名前の仮引数が存在するので、ai モジュールを ai_module という名前でインポートする。gui_play について忘れた方は以前の記事を復習すること
  • 3、7、 8 行目:デフォルト値を None とする仮引数 params を追加し、paramsNone の場合は [{}, {}] を代入する
  • 9 行目play メソッドの呼び出し時にキーワード引数 params=params を追加する
1  import ai as ai_module
2
3  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
4      # ai が None の場合は、人間どうしの対戦を行う
5      if ai is None:
6          ai = [None, None]
7      if params is None:
8          params = [{}, {}]
元と同じなので省略
9      mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
行番号のないプログラム
import ai as ai_module

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    if params is None:
        params = [{}, {}]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": "人間"}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = getattr(ai_module, ai_name)
    
    mb = Marubatsu()
    mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
修正箇所
import ai as ai_module

-def gui_play(ai=None, ai_dict=None, seed=None):
+def gui_play(ai=None, params=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
+   if params is None:
+       params = [{}, {}]
元と同じなので省略
-   mb.play(ai=ai, ai_dict=ai_dict, seed=seed, gui=True)
+   mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)

実行結果は省略しますが、上記の修正後に下記のプログラムを実行すると、人間 VS ai_gt6 で GUI の対戦を行うことができます。

gui_play(ai=[None, ai_gt6], params=[{}, param])

実引数を記述しない場合の処理の修正

現状では、gui_play() のように 実引数を記述しなかった場合 は、Dropdown に ai_gt6 が登録されない ので、下記のプログラムのように登録することにします。

  • 8 行目:Dropdown に登録する AI の一覧を表す ai_dict"ai_gt6" のキーの値に ai_gt6 を代入する
1  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
2      # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
3      if ai_dict is None:
4          ai_dict = { "人間": "人間"}
5          for i in range(1, 15):
6              ai_name = f"ai{i}s"  
7              ai_dict[ai_name] = getattr(ai_module, ai_name)
8          ai_dict["ai_gt6"] = ai_gt6
元と同じなので省略    
行番号のないプログラム
def gui_play(ai=None, params=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    if params is None:
        params = [{}, {}]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": "人間"}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = getattr(ai_module, ai_name)
        ai_dict["ai_gt6"] = ai_gt6
    
    mb = Marubatsu()
    mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
修正箇所
def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": "人間"}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = getattr(ai_module, ai_name)
+       ai_dict["ai_gt6"] = ai_gt6
元と同じなので省略    

上記の修正後に下記のプログラムを実行すると、実行結果のように Dropdown の中に ai_gt6 が表示されるようになります。

gui_play()

しかし、〇 のプレイヤーの Dropdown に ai_gt6 を選択して変更ボタンをクリックする と下記のように エラーが発生 します。

略
File c:\Users\ys\ai\marubatsu\113\marubatsu.py:396, in Marubatsu.play_loop(self, mb_gui, params)
    394 # ai が着手を行うかどうかを判定する
    395 if ai[index] is not None:
--> 396     x, y = ai[index](self, **params[index])
    397 else:
    398     # キーボードからの座標の入力
    399     coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")

TypeError: ai_gt6() missing 1 required positional argument: 'bestmoves_by_board'

このエラーは、今回の記事の最初で説明したエラーと同じものです。また、このエラーが発生する理由以下の通りです。

  • gui_play() を実行すると、下記のプログラムの 5、6 行目で params[{}, {}] が代入されて 7 行目の play メソッドが呼び出される
  • その後、GUI で 〇 または × の担当を ai_gt6 に変更した際に、params の値は [{}, {}] のまま変化しない
  • そのため、x, y = ai[index](self, **params[index]) によって ai_gt6 を呼び出した際に params[index] の値は {} であるため、bestmoves_by_board のキーが存在しない
1  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
2      # ai が None の場合は、人間どうしの対戦を行う
3      if ai is None:
4          ai = [None, None]
5      if params is None:
6          params = [{}, {}]

7    mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)

従って、このエラーの修正には、GUI で 〇 または × の 担当を別の AI に変更した際にparams の要素をその AI に必要なパラメータのデータに変更する処理が必要 となります。

AI が必要とするパラメーターに関する修正

AI の担当を変更する処理は、Marubatsu_GUI クラスの create_event_handler メソッド内で定義された、「変更」ボタンをクリックした際に実行される on_change_button_clicked 内で行われます。従って、Dropdown に登録した それぞれの AI に必要なパラメータのデータMarubatsu_GUI クラスの インスタンスを作成する際に、実引数で渡す必要 があります。

また、Marubatsu_GUI クラスのインスタンス は Marubatsu クラスの play メソッド内で作成される ので、gui_play から play メソッドを呼び出す際にそのデータを実引数で渡す必要 があります。そこで、それぞれの AI に必要なパラメータのデータを どのようなデータ構造play メソッドと Marubatsu_GUI クラスのインスタンスに渡せばよいかについて少し考えてみて下さい。

AI が必要とするパラメータのデータを表すデータ構造

簡単に思いつく方法として、Marubatsu クラスの play メソッドと Marubatu_GUI クラスの __init__ メソッドに params_dict という仮引数を追加 し、その仮引数に ai_dict と同様に dict の形式でそれぞれの AI のパラメーターを代入する という方法が考えられます。例えば、ai2sai_gt2 の 2 つの AI に対するデータは、下記のような dict で表現します。

params_dict = {
    "ai2s": {},    # ai1s はパラメータを必要としないので空の dict
    "ai_gt6": { "bestmoves_by_board": bestmoves_by_board },
}

ただし、この方法では play メソッドと、Marubatsu_GUI クラスの __init__ メソッドに params_dict という 仮引数を追加する必要がある点が面倒 です。

本記事では GUI の Dropdown で選択できる AI の一覧を表す ai_dict のキーの値 に、(AI の関数, AI のパラメーター) という tuple を代入する ように修正することで、ai_dictAI とその AI に必要なパラメーターまとめて記録する ことにします。そのようにすることで、play メソッドと __init__ メソッドに仮引数を追加する必要はなくなります。例えば、ai2sai_gt2 の 2 つの AI に対するデータは、下記のような dict で表現します。

ai_dict = {
    "ai2s": (ai2s, {}),
    "ai_gt6", (ai_gt6, { "bestmoves_by_board": bestmoves_by_board }),
}

gui_play の修正

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

  • 5、8 行目:人間と ai_1s ~ ai14s に対する ai_dict のキーの値に (AI の関数, AI のパラメータ) という tuple を代入するように修正する。その際に、人間とそれらの AI にはパラメータは必要がないので、パラメータのデータとして空の dict を記述する
  • 9、10 行目ai_gt6 のパラメータをファイルから読み込み、ai_dict"ai_gt6" のキーに ai_gt6 とそのパラメータを要素とする tuple を代入する
 1  from util import load_bestmoves
 2
 3  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
 4      if ai_dict is None:
 5          ai_dict = { "人間": ("人間", {}) }
 6          for i in range(1, 15):
 7              ai_name = f"ai{i}s"  
 8              ai_dict[ai_name] = (getattr(ai_module, ai_name), {})
 9          bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
10          ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
元と同じなので省略    
行番号のないプログラム
from util import load_bestmoves

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    if params is None:
        params = [{}, {}]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": ("人間", {}) }
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = (getattr(ai_module, ai_name), {})
        bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
        ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
    
    mb = Marubatsu()
    mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
修正箇所
from util import load_bestmoves

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
    if ai_dict is None:
-       ai_dict = { "人間": "人間" }
+       ai_dict = { "人間": ("人間", {}) }
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
-           ai_dict[ai_name] = getattr(ai_module, ai_name)
+           ai_dict[ai_name] = (getattr(ai_module, ai_name), {})
+       bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
-       ai_dict["ai_gt6"] = ai_gt6
+       ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
元と同じなので省略    

上記の 5 行目の tuple の外側の () を省略して下記のプログラムのように記述することはできない 点に注意して下さい。実際に上記の 5 行目を抜き出した下記のプログラムを実行すると、実行結果のようなエラーが発生します。

ai_dict = { "人間": "人間", {} }

実行結果

  Cell In[14],   line 1
    ai_dict = { "人間": "人間", {} }
                             ^
SyntaxError: ':' expected after dictionary key

上記がエラーになる理由は、, tuple の要素を区切る記号ではなく、dict の要素を区切る記号として解釈される からです。具体的には上記のプログラムは下記のプログラムのように解釈され、3 行目 では {} が dict のキーであると解釈される ため、キー(key)として解釈された {} の後(after)に : が記述されていないという意味のエラーメッセージが表示された文法エラーが発生します。

1  ai_dict = {
2      "人間": "人間",
3      {}
4  }

なお、上記の 8 行目の tuple は外側の () を省略してもプログラムは正しく動作しますが、tuple であることを明確にするために () を記述しました。外側の () を省略した場合に tuple であるとプログラムが解釈するかどうかに自信が持てない場合外側の () は省略しないほうが良い でしょう。

上記で「キーと解釈された{} の後に : が記述されていないというエラーメッセージが表示されている」と説明しましたが、エラーの場所を表す 3 行目の ^ の位置{} ではなく "人間," の下に表示されている点 が気になった人はいないでしょうか‽

  Cell In[14],   line 1
    ai_dict = { "人間": "人間", {} }
                             ^
SyntaxError: ':' expected after dictionary key

エラーメッセージの 3 行目の ^本来は 2 行目の {} 真下の位置に表示されるはず なのですが、2 行目の 人間 という文字が 全角文字であるため、^ の位置が 左にずれて表示 されています。具体的には、3 行目の ^ は、下記の手順で {} の真下に表示しようとします。

  1. 2 行目の {}先頭から何文字目であるか を数える
  2. 3 行目では、上記で数えた 文字数分の半角の空白の後 に ^ を表示する

上記の手順では、全角文字 である 人間2 文字分 と数えられますが、3 行目ではそれに対応して 半角文字が 2 文字分だけ表示 されます。全角文字の横幅 は半角文字の 2 倍で表示される ため、その分だけ ^ の表示位置がずれてしまいます

例えば、上記のプログラムの 人間 を下記のプログラムのように 半角の human のようにする と、^ の位置がずれなくなる ので実行結果のように {} の真下に表示される ようになります。このように、プログラムの中に 全角文字を記述するとエラーメッセージの表記がずれる場合がある 点に注意して下さい。

ai_dict = { "human": "human", {} }

実行結果

  Cell In[15],   line 1
    ai_dict = { "human": "human", {} }
                                   ^
SyntaxError: ':' expected after dictionary key

上記の修正によって、ai_dict のデータ構造が変化したので、下記のプログラムで gui_play を実行するとエラーが発生すると思うかもしれませんが、下記のプログラムを実行しても すぐにエラーは発生せず、実行結果のように Dropdown も正しく表示されます

gui_play()

実行結果

エラーが発生しない理由は以下の通りです。

  • gui_play を実行した直後は 人間 VS 人間 の対戦になっており、AI の関数が呼び出されることはない ので ai_dict のデータ構造が変化しても影響はない
  • Dropdown は create_dropdown の中の下記のプログラムで作成される。この中で、Dropdown の各項目に表示する項目名と項目の値は、3 行目の options=self.ai_dict で決められる。Dropdown の options 属性について忘れた方は以前の記事を復習すること
  • Dropdown の 項目名(ラベル)は、ai_dict キーの値ではなく、キーの名前から作成される ので、ai_dict のキーの値のデータ構造が変化しても Dropdown の項目名は 問題なく作成できる
1  self.dropdown_list.append(
2      widgets.Dropdown(
3          options=self.ai_dict,
4          description=description,
5          layout=widgets.Layout(width="100px"),
6          style={"description_width": "20px"},
7          value=select_values[i],
8      )
9  )  

しかし、Dropdown の 項目の値データ構造 が AI の関数から、(AI の関数、AI のパラメータ) という tuple に変更したため、AI を選択して変更ボタンをクリックすると下記のような エラーが発生 します。

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[5], line 83
     81 for i in range(2):
     82     self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
---> 83 self.mb.play_loop(self, self.params)

File c:\Users\ys\ai\marubatsu\113\marubatsu.py:387, in Marubatsu.play_loop(self, mb_gui, params)
    384 if gui:
    385     # AI どうしの対戦の場合は画面を描画しない
    386     if ai[0] is None or ai[1] is None:
--> 387         mb_gui.update_gui()
    388     # 手番を人間が担当する場合は、play メソッドを終了する
    389     if ai[index] is None:

File c:\Users\ys\ai\marubatsu\113\marubatsu.py:972, in Marubatsu_GUI.update_gui(self)
    970 names = []
    971 for i in range(2):
--> 972     names.append("人間" if ai[i] is None else ai[i].__name__)
    973 ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    975 # ゲームの決着がついていない場合は、手番を表示する

AttributeError: 'tuple' object has no attribute '__name__'

エラーメッセージから、Marubatsu_GUI クラスの update_gui メソッド内で names.append("人間" if ai[i] is None else ai[i].__name__) を実行した際に、tuple に __name__ 属性が存在しない ことがわかり、ai[i] に AI の関数ではなく、tuple が代入される ようになっていることがエラーの原因であることがわかります。

そこで、エラーメッセージをさかのぼって ai 属性に値を代入する処理を探してみる と、下記のエラーメッセージの部分で、marubatsu.ipynb の 5 番のセルの 82 行目の処理で Dropdown に 選択されている項目の値 を表す self.dropdown_list[i].valueself.mb.ai[i] に代入されていることが確認できます。

なお、下記のエラーメッセージの 82 行目は長いので途中で改行しました。

Cell In[5], line 83
     81 for i in range(2):
     82     self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" 
                                 else self.dropdown_list[i].value
---> 83 self.mb.play_loop(self, self.params)

修正前の Dropdown の項目の値は AI の関数でしたが、先程の修正で Dropdown の項目には tuple が代入されるようになったため、上記のエラーが発生したことが確認できました。

Marubatsu_GUI クラスの create_event_handler の修正

上記のプログラムは、create_event_handler 内の下記のプログラムです。

1  # 変更ボタンのイベントハンドラを定義する
2  def on_change_button_clicked(b):
3      for i in range(2):
4          self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" \
5                               else self.dropdown_list[i].value
6     self.mb.play_loop(self, self.params)

Dropdown の 項目の値を表す self.dropdwon_list[i].value には先程の修正で (AI の関数, AI のパラメータ) という tuple が代入 されるようになったので、上記のプログラムの 4 行目を下記のプログラムのように、tuple の各要素の値適切な属性の要素に代入する ことでこのエラーを修正することができます。ただし、下記のプログラムには 1 箇所間違っている点があります。どこが間違っているかについて少し考えてみて下さい。

self.mb.ai[i], self.params[i] = None if self.dropdown_list[i].value == "人間" \
                                     else self.dropdown_list[i].value
修正箇所
-self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" \
+self.mb.ai[i], self.params[i] = None if self.dropdown_list[i].value == "人間" \
                                      else self.dropdown_list[i].value

上記の = の右の式は下記の表のように self.dropdown_list[i].value の値が "人間" の場合は None が、そうでない場合は self.dropdown_list[i].value が計算されるため、表のように それぞれの場合 に計算されるデータの データ型が異なる点が間違っています

self.dropdown_list[i].value の値 式の値 データ型
"人間" None None
それ以外 self.dropdown_list[i].value tuple

そのため、上記の 4 行目の式は、self.dropdown_list[i].value の値が "人間" の場合 は下記のプログラムが実行されるため エラーが発生してしまいます

self.mb.ai[i], self.params[i] = None

従って、上記の = の右の式は self.dropdown_list[i].value の値が "人間" の場合も tuple を計算する ように修正する必要があります。人間が担当する場合は AI のパラメータは必要ないので、本記事ではパラメータの部分に 空の dict を代入した (None, {}) という tuple を計算することにします。具体的には上記の 4 行目を下記のプログラムのように修正します。

self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value
修正箇所
-self.mb.ai[i], self.params[i] = None if self.dropdown_list[i].value == "人間" \
+self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value

上記の (None, {}) を下記のプログラムのように tuple の外側の () を省略することはできない点に注意して下さい2

self.mb.ai[i], self.params[i] = None, {} if self.dropdown_list[i].value == "人間" \
                                         else self.dropdown_list[i].value

上記のプログラムの = の右の式は、下記のような tuple だと解釈されてプログラムが実行されてしまいます。

self.mb.ai[i], self.params[i] = (None, 
   {} if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value)

そのため、上記のプログラムは、下記のプログラムと同じ処理が行われ、self.mb.ai[i] には必ず None が代入される ため、Dropdown に どの項目を選択しても 人間 が選択されたことになってしまいます

self.mb.ai[i] = None
self.params[i] = {} if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value

先程も説明しましたが、tuple の外側の () の省略に関しては注意が必要です。訳が分からなくなった方は、外側の () は常に記述するという方針でも良いと思います

下記は、create_event_handler を修正したプログラムです

1  def create_event_handler(self):
元と同じなので省略
2      # 変更ボタンのイベントハンドラを定義する
3      def on_change_button_clicked(b):
4          for i in range(2):
5              self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
6                                                         else self.dropdown_list[i].value
7          self.mb.play_loop(self, self.params)
元と同じなので省略    
8    
9  Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
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"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value               
                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):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[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,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

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

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

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_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] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                                       else 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
修正箇所
def create_event_handler(self):
元と同じなので省略
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
-           self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" \
+           self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                 else self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)
元と同じなので省略    
    
Marubatsu_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、Dropdown に様々な項目を選択して対戦を行った際に正しくプログラムが処理されることを確認して下さい。

gui_play()

対戦カードの表示場所の微修正

gui_play()ai_gt6 どうしの対戦を行うと下記のようにゲーム盤の下に表示される 対戦カードが右に寄ってしまう 点が気になったので修正することにします。

上記を修正する方法として、ゲーム盤の下に表示される対戦カードの文字列を、ゲーム盤の x 座標の真ん中を基準として、中央揃えで表示する という方法が考えられます。

Axes に文字を表示する text メソッド には 横方向(horizontal)の文字の揃え(alignment)を指定 する horizontalalignment(または ha でも良い)という仮引数があり、下記の値を代入することで text メソッドで指定した文字列が、text メソッドで指定した座標に対して下記の表の揃えで表示されるようになります。

意味
left(デフォルト値) 左揃えで表示する
center 中央揃えで表示する
right 右揃えで表示する

同様に、縦方向の文字の揃えを代入する verticalalignment(または va)という仮引数もあります。他にも様々な指定を行うことができる仮引数があるので興味がある方は下記のリンク先を参照して下さい。

ゲーム盤の左端と右端の x 座標は 0 と 3 なので、ゲーム盤の中央の x 座標は 1.5 になります。従って、対戦カードを表示する update_gui を下記のプログラムのように修正することで対戦カードをゲーム盤の中央の位置に表示することができるようになります。

  • 6 行目text メソッドの x 座標を 0 から 1.5 に修正し、その位置に中央揃えで表示されるように ha="center" を記述する
1  def update_gui(self):
元と同じなので省略
2      # 対戦カードの文字列を計算する
3      names = []
4      for i in range(2):
5          names.append("人間" if ai[i] is None else ai[i].__name__)
6      ax.text(1.5, 3.5, f"{names[0]} VS {names[1]}", fontsize=20, ha="center")   
元と同じなので省略
7    
8  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)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(1.5, 3.5, f"{names[0]} VS {names[1]}", 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() 
    
Marubatsu_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
-   ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
+   ax.text(1.5, 3.5, f"{names[0]} VS {names[1]}", fontsize=20, ha="center")   
元と同じなので省略
    
Marubatsu_GUI.update_gui = update_gui

下記の実行結果は、上記の修正後に下記のプログラムで gui_play を実行し、様々な対戦カードを設定した場合の表示です。いずれもゲーム盤の中に中央揃えで対戦カードがバランスよく表示されることが確認できます。

gui_play()

実行結果

  

今回の記事のまとめ

今回の記事では、パラメータが必要な AI を GUI で対戦できるように修正し、gui_playai_gt6 を選択できるように修正しました。

実は今回の記事で修正した Marubatsu_GUI にはいくつかのバグがあるので次回の記事で修正することにします。どのようなバグがあるかについて余裕がある方は考えてみて下さい。

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

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

次回の記事

  1. 他の強解決の AI である ai_gt1 ~ ai_gt5 でも同様の結果になります。ai_gt6 にしたのは AI のパラメーターのデータサイズが最も小さいため、ファイルからのデータの読み込み時間が最も短くなるからです

  2. 筆者は最初に () を省略したプログラムを記述してしまい、Dropdown に何を選択しても 人間 VS 人間 になってしまうという論理エラーが発生してしまいました

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