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

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/847598556d7e49fe2d87

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

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

| リンク | 説明 |
|:--|:--|
| [marubatsu.py](https://github.com/ysgeso/marubatsu/blob/master/113/marubatsu.py)  | Marubatsu、Marubatsu_GUI クラスの定義|
| [ai.py](https://github.com/ysgeso/marubatsu/blob/master/113/ai.py)  |  AI に関する関数 |
| [test.py](https://github.com/ysgeso/marubatsu/blob/master/113/test.py)  |  テストに関する関数 |
|  [util.py](https://github.com/ysgeso/marubatsu/blob/master/113/util.py) |  ユーティリティ関数の定義。現在は `gui_play` のみ定義されている |
| [tree.py](https://github.com/ysgeso/marubatsu/blob/master/113/tree.py)  | ゲーム木に関する Node、Mbtree クラスの定義  |
| [gui.py](https://github.com/ysgeso/marubatsu/blob/master/113/gui.py)  |  GUI に関する処理を行う基底クラスとなる GUI クラスの定義  |

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

https://qiita.com/ysgeso/items/10d1d01192c014173e4b

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

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

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

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

```python
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 で対戦すると、実行結果のように対戦結果が表示されます。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e1e58f7a-113d-0390-2b42-4056aac71fda.png" width="200">

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

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

```python
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 が代入されているので、上記のようなエラーが発生するのはおかしいと思えるかもしれません。

```python
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)
```

:::note
エラーメッセージの 2 行目の create_event_handler.&lt;locals&gt;.on_mouse_down の __&lt;locals&gt;__ は、その後ろに記述された `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` には `[{}, {}]` が代入される__ ことになります。

```python
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` というキーを持たない__ ことが確認できました。

このエラーは[以前の記事](https://qiita.com/ysgeso/items/321cf89ef5c49b861650#%E3%83%90%E3%82%B0%E3%81%AE%E4%BF%AE%E6%AD%A3)で、__`play_loop` メソッドに仮引数 `params` を追加するように修正した際__ に、Marubatsu_GUI クラス内で __`play_loop` メソッドを呼び出す処理を修正するのを後回しにしたことが原因__ です。

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

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

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


```python
 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` を呼び出すことはできません__。

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

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

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

* __5 ~ 7 行目__：デフォルト値を `None` とする仮引数 `params` を追加し、`params` が `None` の場合に `params` に `[{}, {}]` を代入する
* __8、9 行目__：この処理は `__init__` の後半に記述していたが、仮引数が `None` の場合の処理は先頭に記述するのが一般的なのでこちらに移動した
* __12 行目__：仮引数 `params` を同名の属性に代入することで、__Marubatsu_GUI クラスの中で仮引数 `params` の値をどこでも利用できる__ ようになる
* __16 行目__：この行の修正については、この後で説明する

```python
 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__
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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__
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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__
```
</details>

### `super` の修正に関する説明

[以前の記事](https://qiita.com/ysgeso/items/2acd8c5ceb8e9cda7233#%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%A9%E3%82%A4%E3%83%89%E3%81%AE%E7%9B%AE%E7%9A%84%E3%81%A8%E5%9F%BA%E5%BA%95%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97)で、__`super`__ は「クラスの定義の中で記述されたメソッドの中でしか利用できない」と説明しましたが、実は下記のように記述することで、__クラスの定義の外__ で記述されたメソッドの中でも __利用できる__ ことがわかりました。

```python
super(クラス名, self)
```

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

```python
super(Marubatsu_GUI, self).__init__()
```

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

https://docs.python.org/ja/3/library/functions.html#super

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

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

```python
 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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>

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

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

```python
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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>

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

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

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

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

# `gui_play` の修正

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

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

```python
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` について忘れた方は[以前の記事](https://qiita.com/ysgeso/items/1dbfec99a08b8996f53c#gui_play-%E3%81%AE%E5%AE%9A%E7%BE%A9)を復習すること
* __3、7、 8 行目__：デフォルト値を `None` とする仮引数 `params` を追加し、`params` が `None` の場合は `[{}, {}]` を代入する
* __9 行目__：`play` メソッドの呼び出し時にキーワード引数 `params=params` を追加する

```python
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)
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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)
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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)
```
</details>

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

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

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

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

* __8 行目__：Dropdown に登録する AI の一覧を表す `ai_dict` の `"ai_gt6"` のキーの値に `ai_gt6` を代入する 

```python
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
元と同じなので省略    
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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)
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
元と同じなので省略    
```
</details>

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

```python
gui_play()
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b635b259-cdbf-3754-d00f-eaa855903449.png" width="150">

しかし、〇 のプレイヤーの 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` のキーが存在しない__

```python
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 のパラメーターを代入する__ という方法が考えられます。例えば、`ai2s` と `ai_gt2` の 2 つの AI に対するデータは、下記のような dict で表現します。

```python
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_dict`__ で __AI とその AI に必要なパラメーター__ を __まとめて記録する__ ことにします。そのようにすることで、`play` メソッドと __`__init__`__ メソッドに仮引数を追加する必要はなくなります。例えば、`ai2s` と `ai_gt2` の 2 つの AI に対するデータは、下記のような dict で表現します。

```python
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 を代入する

```python
 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})
元と同じなので省略    
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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)
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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})
元と同じなので省略    
```
</details>

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

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

実行結果

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

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

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

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

:::note
上記で「キーと解釈された`{}` の後に `:` が記述されていないというエラーメッセージが表示されている」と説明しましたが、__エラーの場所を表す 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 のようにする__ と、__^ の位置がずれなくなる__ ので実行結果のように __`{}` の真下に表示される__ ようになります。このように、プログラムの中に __全角文字を記述するとエラーメッセージの表記がずれる場合がある__ 点に注意して下さい。

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

実行結果

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

:::


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

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b635b259-cdbf-3754-d00f-eaa855903449.png" width="150">

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

* `gui_play` を実行した直後は __人間 VS 人間__ の対戦になっており、__AI の関数が呼び出されることはない__ ので `ai_dict` のデータ構造が変化しても影響はない
* Dropdown は `create_dropdown` の中の下記のプログラムで作成される。この中で、Dropdown の各項目に表示する項目名と項目の値は、3 行目の __`options=self.ai_dict`__ で決められる。Dropdown の `options` 属性について忘れた方は[以前の記事](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#options)を復習すること
* Dropdown の __項目名__（ラベル）は、__`ai_dict` の__ キーの値ではなく、__キーの名前から作成される__ ので、`ai_dict` のキーの値のデータ構造が変化しても Dropdown の項目名は __問題なく作成できる__

```python
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].value` が `self.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` 内の下記のプログラムです。

```python
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 箇所間違っている点があります。どこが間違っているかについて少し考えてみて下さい。

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

<details><summary><strong>修正箇所</strong></summary>

```diff_python
-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
```
</details>

上記の `=` の右の式は下記の表のように `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` の値が `"人間"` の場合__ は下記のプログラムが実行されるため __エラーが発生してしまいます__。

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

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

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

<details><summary><strong>修正箇所</strong></summary>

```diff_python
-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
```
</details>

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

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

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

```python
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 に __どの項目を選択しても 人間 が選択されたことになってしまいます__。

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

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

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

```python
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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>

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

```python
gui_play()
```

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b95da805-cefc-1031-5946-4aeb7dee36c2.png" width="200">

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

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

| 値 | 意味 |
|:-:|:-:|
| `left`（デフォルト値）| 左揃えで表示する  |
| `center`  | 中央揃えで表示する  |
| `right`  | 右揃えで表示する  |

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

https://matplotlib.org/stable/api/text_api.html#module-matplotlib.text

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

* __6 行目__：`text` メソッドの　x 座標を 0 から 1.5 に修正し、その位置に中央揃えで表示されるように `ha="center"` を記述する

```python
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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>

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

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3cb9ea98-9b66-2dd9-3e95-e9e831543360.png" width="180">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/555fe718-22f8-acc5-8f40-04e433e1d780.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f76356ba-8b62-f891-6709-f274d1aa8311.png" width="200">

# 今回の記事のまとめ

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

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

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

| リンク | 説明 |
|:--|:--|
| [marubatsu.ipynb](https://github.com/ysgeso/marubatsu/blob/master/113/marubatsu.ipynb)  | 本記事で入力して実行した JupyterLab のファイル |
| [marubatsu_new.py](https://github.com/ysgeso/marubatsu/blob/master/113/marubatsu_new.py)  | 今回の記事で更新した marubatsu.py |
| [util_new.py](https://github.com/ysgeso/marubatsu/blob/master/113/util_new.py)  | 今回の記事で更新した util.py |

# 次回の記事

https://qiita.com/ysgeso/items/0894a987b51323a4a9b4

[^1]:他の強解決の AI である `ai_gt1` ~ `ai_gt5` でも同様の結果になります。`ai_gt6` にしたのは AI のパラメーターのデータサイズが最も小さいため、ファイルからのデータの読み込み時間が最も短くなるからです
[^2]:筆者は最初に `()` を省略したプログラムを記述してしまい、Dropdown に何を選択しても 人間 VS 人間 になってしまうという論理エラーが発生してしまいました