Pythonで〇×ゲームのAIを一から作成する　その86　GUI による乱数の種の設定と対戦結果のファイルへの読み書き

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/b8c4948504e7d11a75d4

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

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

https://github.com/ysgeso/marubatsu/blob/master/086/marubatsu.py

https://github.com/ysgeso/marubatsu/blob/master/086/ai.py

https://github.com/ysgeso/marubatsu/blob/master/086/util.py

## ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

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


# 乱数の種の設定

今回の記事では、前回の記事から引き続き、〇×ゲームの GUI の改良を行います。

__`play`__ メソッドでは、キーワード引数 `seed` で __[乱数の種](https://qiita.com/ysgeso/items/b8b4123e3919357583d7#%E4%B9%B1%E6%95%B0%E3%81%AE%E7%A8%AE)__ を __設定することができます__ が、現状では GUI で〇×ゲームを遊ぶ際に、__後から乱数の種を設定することはできません__。そこで、GUI で 〇× ゲームを遊ぶ際に、__後から乱数の種を設定できる__ ようにすることにします。

## 乱数の種を設定する GUI

まず、__どのような GUI で乱数の種を設定するか__ を決める必要があります。本記事では、下記のような GUI で乱数の種を設定することにします。

* __乱数の種を適用するか__ どうかを選択する __チェックボックス__ を用意する
* __乱数の種を入力するテキストボックス__ を用意し、上記の __チェックボックスが ON__ になっている場合にその中の値を __乱数の種として適用する__ ことにする

## Checkbox の属性と表示

ipywidgets には、__Checkbox__ という、__ON/OFF を選択できるウィジェット__ が用意されています。Checkbox には、主に下記の属性があります。

| 属性 | 意味 | デフォルト値 |
|:--|:--|--:|
| __`value`__  | ON/OFF を表す Boolean 型のデータ  | `False` |
| __`description`__  | 右に表示される説明  | `""` |
| __`indent`__ | `True` の場合に右にずらして<br>（インデントして）表示する | `True` |
| __`disabled`__ | `True` の場合に操作できなくなる | `False` |

下記は、Checkbox を作成して表示するプログラムの例です。なお、__`indent=False` を記述しないと__、Checkbox が __右にずれて表示される__ 点に注意して下さい。

```python
import ipywidgets as widgets

display(widgets.Checkbox(value=True, description="乱数の種", indent=False))
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a98e2fb5-8850-64e1-ac8e-c0356329e68e.png" width="100">

Checkbox の詳細については、下記のリンク先を参照して下さい。

https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#checkbox

## IntText の属性と表示

ipywidgets には、さまざまな種類のテキストボックスが用意されていますが、__乱数の種__ には __整数を指定する必要がある__ ので、それらの中の __IntText__ という、__整数のみを入力できるウィジェット__ を利用することにします。IntText には、主に下記の属性があります。

| 属性 | 意味 | デフォルト値 |
|:--|:--|--:|
| __`value`__  | ON/OFF を表す Boolean 型のデータ  | `0` |
| __`description`__  | 左に表示される説明  | `""` |
| __`disabled`__ | `True` の場合に操作できなくなる | `False` |

下記は、IntText を作成して表示するプログラムの例です。

```python
display(widgets.IntText())
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/672d19dc-8653-6dcc-cddc-595058f62cec.png" width="250">

IntText に対しては、下記の操作を行うことができます。

* 右にある上下の三角形のボタンで数値を増減できる
* 数値を入力する部分をクリックして選択後に、キーボードで直接整数を入力できる[^1]

IntText の詳細については、下記のリンク先を参照して下さい。

https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#inttext

:::note
他にも、整数以外の数値を入力できる FloatText、文字列を入力できる Text などがあります。詳細は下記のリンク先を参照して下さい。
:::

https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#floattext

https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#text


## Checkbox と IntText の作成と配置

本記事では、Checkbox と IntText を __リセットボタンなどの上部に配置__ することにします。また、__`play`__ メソッドの __仮引数 `seed` の値__ によって、Checkbox と IntText の __初期状態を下記のように設定する__ ことにします。

| `seed` の値 | `Checkbox` | `IntText` の値 |
|:--|:--|--:|
| __`None`__  | OFF にする  | `0`  |
| __`None` 以外__  |  ON にする | `seed` の値  |

### `Marubatsu_GUI` クラスへの `seed` 属性の追加

そのためには、__`Marubatsu_GUI`__ のインスタンスが __`play`__ メソッドの __仮引数 `seed` の値を知る必要__ があるので、下記のプログラムのように、__`Marubatsu_GUI`__ の `__init__` メソッドに __仮引数 `seed` を追加し__、`Marubatsu_GUI` クラスの __`seed` 属性に代入する__ ことにします。

* __3 行目__：デフォルト値を `None` とする仮引数 `seed` を追加する
* __7 行目__：`Marubatsu_GUI` のインスタンスの `seed` 属性に、仮引数 `seed` を代入する

```python
1  from marubatsu import Marubatsu_GUI
2
3  def __init__(self, mb, ai_dict=None, seed=None, size=3):   
元と同じなので省略
4      self.mb = mb
5      self.ai_dict = ai_dict
6      self.seed = seed
元と同じなので省略
7
8  Marubatsu_GUI.__init__ = __init__```
```

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

```python
from marubatsu import Marubatsu_GUI

def __init__(self, mb, ai_dict=None, seed=None, size=3):   
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}

    self.mb = mb
    self.ai_dict = ai_dict
    self.seed=seed
    self.size = size

    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        self.ai_dict = {}

    # %matplotlib widget のマジックコマンドを実行する
    get_ipython().run_line_magic('matplotlib', 'widget')
    
    self.create_widgets()
    self.create_event_handler()
    self.display_widgets() 

Marubatsu_GUI.__init__ = __init__
```
</details>

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

```diff_python
from marubatsu import Marubatsu_GUI

-def __init__(self, mb, ai_dict=None, size=3):   
+def __init__(self, mb, ai_dict=None, seed=None, size=3):   
元と同じなので省略
    self.mb = mb
    self.ai_dict = ai_dict
+   self.seed = seed
元と同じなので省略

Marubatsu_GUI.__init__ = __init__
```
</details>

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

* __6 行目__：キーワード引数 `seed=seed` を追加する

```python
1  from marubatsu import Marubatsu
2
3  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
4      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
5      if gui:
6          mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, seed=seed, size=size)  
元と同じなので省略
7
8  Marubatsu.play = play
```

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

```python
from marubatsu import Marubatsu

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.params = params
    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, ai_dict=ai_dict, seed=seed, size=size)  
    else:
        mb_gui = None
        
    self.restart()
    return self.play_loop(mb_gui)

Marubatsu.play = play
```
</details>

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

```diff_python
from marubatsu import Marubatsu

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, size=size)  
+       mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, seed=seed, size=size)  
元と同じなので省略

Marubatsu.play = play
```
</details>

### 値が設定された Checkbox と IntText の作成

次に、下記のプログラムの 3 ~ 6 行目のように `create_widgets` を修正して、__`seed` 属性の値に応じて適切な値が設定__ されたCheckbox と IntText を作成します。

* __3 行目__：`self.seed is not None` は、`seed` 属性が `None` の場合に `False`、そうでない場合に `True` となるので、`value=self.seed is not None` を記述することで、__`seed` 属性が `None` の場合に OFF、そうでない場合に ON__ となる Checkbox を作成する
* __5 行目__：__`seed` 属性が `None` の場合に `0`、そうでない場合に `seed`属性の値が設定__ されている IntText を作成する

```python
1  def create_widgets(self):
2      # 乱数の種の Checkbox と IntText を作成する
3      self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
4                                       indent=False, layout=widgets.Layout(width="100px"))
5      self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
6                                     layout=widgets.Layout(width="100px"))   
元と同じなので省略                                   
7    
8  Marubatsu_GUI.create_widgets = create_widgets
```

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

```python
def create_widgets(self):
    # 乱数の種の Checkbox と IntText を作成する
    self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
                                     indent=False, layout=widgets.Layout(width="100px"))
    self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
                                   layout=widgets.Layout(width="100px"))   
    
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセット、待ったボタンを作成する
    self.change_button = self.create_button("変更", 50)
    self.reset_button = self.create_button("リセット", 80)
    self.undo_button = self.create_button("待った", 60)    
    
    # リプレイのボタンとスライダーを作成する
    self.first_button = self.create_button("<<", 50)
    self.prev_button = self.create_button("<", 50)
    self.next_button = self.create_button(">", 50)
    self.last_button = self.create_button(">>", 50)     
    self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()       
    
Marubatsu_GUI.create_widgets = create_widgets
```
</details>

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

```diff_python
def create_widgets(self):
    # 乱数の種の Checkbox と IntText を作成する
+   self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
+                                    indent=False, layout=widgets.Layout(width="100px"))
+   self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
+                                  layout=widgets.Layout(width="100px"))   
元と同じなので省略                                   
    
Marubatsu_GUI.create_widgets = create_widgets
```
</details>

### Checkbox と IntText の配置

次に、`display_widgets` を下記のプログラムのように修正して Checkbox と IntText を配置して表示するようにします。

* __3 行目__：乱数の種のウィジェットを横に配置した HBox を作成して `hbox1` に代入する
* __5、7 行目__：それまでの `hbox1` と `hbox2` を、`hbox2` と `hbox3` に修正する
* __9 行目__：3 つの Hbox を縦に配置する

```python
 1  def display_widgets(self):
 2      # 乱数の種のウィジェットを横に配置した HBox を作成する
 3      hbox1 = widgets.HBox([self.checkbox, self.inttext])
 4      # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
 5      hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
 6      # リプレイ機能のボタンを横に配置した HBox を作成する
 7      hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
 8      # hbox1 ~ hbox3 を縦に配置した VBox を作成し、表示する
 9      display(widgets.VBox([hbox1, hbox2, hbox3])) 
10    
11  Marubatsu_GUI.display_widgets = display_widgets
```

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

```python
def display_widgets(self):
    # 乱数の種のウィジェットを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox3 を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3])) 
    
Marubatsu_GUI.display_widgets = display_widgets
```
</details>

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

```diff_python
def display_widgets(self):
    # 乱数の種のウィジェットを横に配置した HBox を作成する
+   hbox1 = widgets.HBox([self.checkbox, self.inttext])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
-   hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
+   hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
-   hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
+   hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox3 を縦に配置した VBox を作成し、表示する
-   display(widgets.VBox([hbox1, hbox2])) 
+   display(widgets.VBox([hbox1, hbox2, hbox3])) 
    
Marubatsu_GUI.display_widgets = display_widgets
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行すると、実行結果のように __乱数の種に関するウィジェットが上部に表示される__ ようになります。__`gui_play` から呼び出された `play`__ メソッドには乱数の種を表す __キーワード引数 `seed` を記述していない__ ので、実行結果のように __CheckBox は OFF__ が、__IntText は 0__  が設定された状態で作成されます。

```python
from util import gui_play

gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b06ae4cf-1351-5c76-6d81-0c3372fcda85.png" width="200">

## `gui_play` の修正

現状の __`gui_play` では乱数の種を設定できない__ ので、下記のプログラムのように __`gui_play` に仮引数 `seed` を追加__ して乱数の種を設定できるようにします[^2]。

* __4 行目__：デフォルト値を `None` とする仮引数 `seed` を追加する
* __6 行目__：`play` メソッドの実引数に、キーワード引数 `seed=seed` を追加する

```python
1  import ai as ai_module
2  import random
3
4  def gui_play(ai=None, ai_dict=None, seed=None):
元と同じなので省略
5      mb = Marubatsu()
6      mb.play(ai=ai, ai_dict=ai_dict, seed=seed, gui=True)
```

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

```python
import ai as ai_module
import random

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

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

```diff_python
import ai as ai_module
import random

-def gui_play(ai=None, ai_dict=None):
+def gui_play(ai=None, ai_dict=None, seed=None):
元と同じなので省略
    mb = Marubatsu()
-   mb.play(ai=ai, ai_dict=ai_dict, gui=True)
+   mb.play(ai=ai, ai_dict=ai_dict, seed=seed, gui=True)
```
</details>

上記の修正後に、下記のプログラムで __キーワード引数 `seed=123` を記述__ して `gui_play` を実行すると、実行結果のように、__Checkbox が ON__ になり、__IntText に 123__ が設定されます。

```python
gui_play(seed=123)
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3abbb2ee-2b5c-4c9a-ed28-041d28a5a4f7.png" width="200">

## 乱数の種の設定の処理の実装

__乱数の種の設定の処理__ は、〇×ゲームの __開始時に行う処理__ なので、Checkbox や IntText の __操作を行った際__ に乱数の種の設定の処理を __行う必要はありません__。そのため、Checkbox や IntText に対する __イベントハンドラを記述する必要はありません__[^3]。

また、乱数の種の設定は __`play` メソッドの最初で行われる__ ので、`play` メソッドを実行した直後に開始されるゲームでは、改めて乱数の種の設定を行う必要はありません。従って、乱数の種の設定の処理は、__リセットボタンをクリックした場合のみで行う必要があります__。そこで、リセットボタンのイベントハンドラを下記のプログラムのように修正します。

* __5、6 行目__：乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_reset_button_clicked(b=None):
 4          # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
 5          if self.checkbox.value:
 6              random.seed(self.inttext.value)
 7          self.mb.restart()
 8          on_change_button_clicked(b)
元と同じなので省略
 9   
10  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```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 == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        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.draw_board()
        
    # イベントハンドラをボタンに結びつける
    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.draw_board()        

    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)
            self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    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,            
        }
        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_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
+       if self.checkbox.value:
+           random.seed(self.inttext.value)
        self.mb.restart()
        on_change_button_clicked(b)
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行して `ai14s` VS `ai14s` の対戦を、乱数の種に 123 を設定して行うと、実行結果のような結果になります。この状態で __リセットボタンをクリック__ すると、__同じ乱数の種__ で `ai14s` VS `ai14s` の __対戦が行われる__ ので、__毎回同じ対戦結果__ になります。また、乱数の種の CheckBox を OFF にしてからリセットボタンをクリックすると乱数の種の処理が行われないので、異なる対戦結果になります。

```python
from ai import ai14s

gui_play(ai=[ai14s, ai14s], seed=123)
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/97a40f12-29d2-e7e0-0d1a-a97332cc2a29.png" width="200">

上記の対戦結果が正しいかどうかを確認するために、下記のプログラムで、__CUI で乱数の種に 123 を設定__ して `ai14s` VS `ai14s` の対戦を行ってみます。実行結果から、__GUI と CUI で同じ対戦結果になる__ ことが確認できます。また、GUI でリプレイボタンをクリックして、__途中経過でも同じ着手が行われる__ ことを確認してみて下さい。

```python
mb = Marubatsu()
mb.play(ai=[ai14s, ai14s], seed=123)
```

実行結果

```
略
winner draw
oxO
xoo
xox
```

## Checkbox が OFF の場合の IntText の操作の禁止

上記では、__Checkbox が OFF の場合__ でも __IntText の操作__ を行うことが __できてしまいます__。乱数の種を利用しないのに IntText の操作ができるのは不自然なので、__Checkbox が OFF の場合__ は IntText の `disabled` 属性を `False` にして __操作を行えないよう__ にします。

これは、__IntText の設定を変更する処理__ なので、下記のプログラムのように、__ウィジェットの状態を更新__ する __`update_widgets_status`__ を修正します。

* __2 行目__：Checkbox が OFF（`value` 属性が `False`）になっている場合に、IntText を操作できないように、`disabled` 属性を `True` にする。これは Checkbox の `value` 属性の __`True` と `False` を反転__ する処理なので、__[not 演算子](https://qiita.com/ysgeso/items/1fdd19037bbf64082a42#not-%E6%BC%94%E7%AE%97%E5%AD%90)__ を使って行うことができる

```python
1  def update_widgets_status(self):
2      self.inttext.disabled = not self.checkbox.value
3      self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
元と同じなので省略
4      
5  Marubatsu_GUI.update_widgets_status = update_widgets_status
```

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

```python
def update_widgets_status(self):
    self.inttext.disabled = not self.checkbox.value
    self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
    self.set_button_status(self.prev_button, self.mb.move_count <= 0)
    self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
    self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)    
    # value 属性よりも先に max 属性に値を代入する必要がある点に注意！
    self.slider.max = len(self.mb.records) - 1
    self.slider.value = self.mb.move_count
    
Marubatsu_GUI.update_widgets_status = update_widgets_status
```
</details>

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

```diff_python
def update_widgets_status(self):
+   self.inttext.disabled = not self.checkbox.value
    self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
元と同じなので省略
    
Marubatsu_GUI.update_widgets_status = update_widgets_status
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行すると、実行結果のように、__Checkbox が OFF__ になるので、__IntText が薄く表示__ されて __操作できなくなります__。

```python
gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/07d10c50-fa13-9f14-6d90-06e23eeed928.png" width="200">

:::note
本記事では行いませんが、薄く表示されていることがわかりづらいと思った方は、[ボタンで行った](https://qiita.com/ysgeso/items/94fba56cb97dc4787ca8#%E6%93%8D%E4%BD%9C%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E3%83%9C%E3%82%BF%E3%83%B3%E3%82%92%E6%98%8E%E7%A2%BA%E3%81%AB%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95-%E3%81%9D%E3%81%AE-2)ように、IntText の表示の色を変更するなどの方法が考えられます。
:::

このプログラムには問題があります。それは、上図の状態で、__Checkbox__ をクリックして __ON にしても__、下図のように、__IntText__ が灰色のまま、__操作できない__ というものです。そのようなことが起きる理由について少し考えてみて下さい。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e218d3ee-74a7-c4a8-a65f-8b95aa9b2197.png" width="200">

### 問題の原因と修正

この問題は、IntText の設定を変更する __`update_widgets_status`__ が、ゲーム盤の描画を更新する __`draw_board`__ メソッド __のみから__ 呼び出されているためです。現状では __Checkbox__ に対する __イベントハンドラを記述していない__ ため、__Checkbox をクリック__ しても __何の処理も行われず__、IntText の設定は変更されません。

そこで、下記のプログラムのように、__Checkbox__ をクリックして __`value` 属性が変更__ された際に実行する __`on_checkbox_changed`__ という __イベントハンドラを定義__ し、`observe` メソッドを使って Checkbox に結びつけることにします。

* __3 行目__：Checkbox の ON/OFF が変更された場合のイベントハンドラを定義する
* __4 行目__：`update_widgets_status` メソッドを呼び出すことで、IntText の設定を変更する
* __6 行目__：`observe` メソッドで Checkbox の `value` 属性の値が変更された場合に `on_checkbox_changed` が呼び出されるようにする

```python
1  def create_event_handler(self):
2      # 乱数の種のチェックボックスのイベントハンドラを定義する
3      def on_checkbox_changed(changed):
4          self.update_widgets_status()
5
6      self.checkbox.observe(on_checkbox_changed, names="value")
元と同じなので省略
7    
8  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_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)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        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.draw_board()
        
    # イベントハンドラをボタンに結びつける
    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.draw_board()        

    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)
            self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    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,            
        }
        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_checkbox_changed(changed):
+       self.update_widgets_status()

+   self.checkbox.observe(on_checkbox_changed, names="value")
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

実行結果は省略しますが、上記の修正後に、下記のプログラムで `gui_play` を実行し、__Checkbox をクリック__ することで __IntText が正しい状態になる__ ことを確認して下さい。

```python
gui_play()
```

# 対戦の保存と読込

現状では対戦の終了後にリセットボタンをクリックすると、__それまでの対戦がリセット__ されてしまうため、__後でその対戦を再現することができなくなります__。そこで、対戦の結果をファイルに保存し、後からそのファイルを読み込んで再現できるようにすることにします。

:::note
上記では対戦の終了後にと記述しましたが、対戦途中の局面でも問題なく保存できるようにします。
:::

## ファイルのオープン

ファイルにデータを保存したり、ファイルからデータを読み込むためには、組み込み関数 __`open`__ を使って __ファイルをオープンする__（開く）必要があります。

open の詳細については、下記のリンク先を参照して下さい。

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

ファイルのオープンの処理は、下記のプログラムのように記述します[^4]。

```python
f = open(file, mode)
```

実引数 __`file`__ には、オープンする __ファイルのパス__（住所のこと）を文字列で指定します。__実行した python のプログラムが保存されているフォルダ内のファイル__ であれば、__ファイル名を表す文字列__ を記述すれば良いのですが、それ以外のフォルダ内のファイルの場合は、ファイルのパスの文法を覚える必要があります。

ただし、ファイルのパスは、ファイルを開く（保存）__パネルからファイルを開いた場合__ は __自動的に生成__ されるので、今回の記事ではファイルのパスの詳細な記述方法については説明しません。興味がある方は、ファイルのパスをキーワードに検索すると良いでしょう。

実引数 __`mode`__ には、__ファイルをどのように開くか__ を下記の __文字列の組み合わせで指定__ します[^5]。例えば、__読み込みと書き込みの両方__ を行いたい場合は __`"rw"`__ を指定します。

__バイナリモード__ は、__文字列型以外のデータ__[^6]を保存する場合に指定するモードです。

| 文字列 | 意味 |
|:-:|:--|
| __`"r"`__  | ファイルを読み込みモードで開く  |
| __`"w"`__  | ファイルを書き込みモードで開く  |
| __`"b"`__  | ファイルをバイナリモードで開く  |


`open` は __ファイルの読み書きの処理__ を行う際に利用する、__ファイルオブジェクト__ を返します。上記のプログラムでは `f` という変数にファイルオブジェクトを代入しています。

今回の記事では、この後で説明する __`pickle`__ というモジュールを利用してファイルの読み書きを行うので、ファイルオブジェクトを直接利用したファイルの読み書きの方法については紹介しません。興味がある方は下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/tutorial/inputoutput.html#tut-files

## ファイルのクローズ

`open` で開いたファイルは、__ファイルの読み書きの処理の終了後に閉じる必要があります__。__ファイルを閉じ忘れる__ と、下記のような __問題が発生する可能性__ があります。

* メモリが無駄に消費される
* ファイルの処理の途中でエラーが発生した場合などで __ファイルの内容が破損__ する
* ファイルの __書き込み__ が __正しく反映されない__

ファイルを閉じる処理は、__`f.close()`__ を記述することで行えますが、この処理の __記述のし忘れ__ などによって __ファイルが閉じられないことによるバグが良く発生します__。

## `with` を利用したファイルの処理

ファイルの __閉じ忘れを防ぐ方法__ として、ファイルを開き、ファイルの読み書きの処理が終了した後に、__ファイルを必ず閉じるようにする__、__`with` を使った方法__ が良く使われます。

`with` を使ったファイルの処理は下記のプログラムのように記述します。下記のプログラムでは、__`f`__ に `open` の返り値である __ファイルオブジェクトが代入__ され、__`with` のブロック__ の中の __プログラムの処理の終了後__ に、__自動的__ にファイルが __必ず閉じられる__ ようになります。

```python
with open(file, mode) as f:
    このブロックにファイルに関する処理を記述する
```

`with` が行う処理の内容は初心者とってはかなり難しいと思いますので今回の記事では説明しません。また、__ファイルの読み書き__ を行う際に、__`with`__ の __正確な意味を理解する必要はありません__。当面は __ファイルの読み書きの処理を行う際に記述する決まり文句__ のようなものだと思えば良いでしょう。`with` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/reference/compound_stmts.html#the-with-statement

:::note warn
__ファイルを確実に閉じる処理__ を __正確に記述するのは意外に困難です__。例えば、ファイルの読み書きの処理の __途中でエラーが発生__ した場合は、例外処理を記述しなければ、__その時点でプログラムが終了__ してしまうため、その後でファイルを閉じる処理がプログラムに記述されていたとしても、__ファイルは閉じられません__。

上記の __`with` を利用__ すれば、`with` のブロックの中でエラーが発生した場合でも、__確実にファイルを閉じる処理が実行されます__。
:::

## `pickle` によるデータの保存

python には、__オブジェクトなどの、複雑なデータをファイルに読み書きする__ ための __pickle__ というモジュールがあります。pickle でデータをファイルに保存するには、下記のプログラムのように、`pickle` モジュールの __`dump`__ という関数を利用します。

```python
pickle.dump(obj, file)
```

実引数 __`obj`__ には、__ファイルに保存するオブジェクト__ を記述します。`pickle` は、クラスのインスタンスを含む、__Python の多くのオブジェクト__ をファイルに保存できます。

__`pickle`__ はデータを（文字列ではない）__バイナリデータで保存__ するので、実引数 __`file`__ には、`open` で __書き込みモード__ と __バイナリモード__ の両方を表す __`"wb"`__ を指定して開いた __ファイルオブジェクト__ を記述する必要があります。

下記は、__test.pkl__ というファイル[^7]に `[1, 2, 3]` という list を保存するプログラムです。3 行目で、先程説明した __`with`__ を使ってファイルを開いているので、この `with` のブロックの処理の完了後に __必ず開いたファイルが閉じられます__。

下記のプログラムを実行しても何も表示は行われませんが、__`marubatsu.ipynb` と同じフォルダ__ に test.pkl という名前の __ファイルが作成される__ ので確認して下さい。

```python
import pickle

with open("test.pkl", "wb") as f:
    pickle.dump([1, 2, 3], f) 
```

なお、__`pickle`__ で作成したファイルは、__ダブルクリックして開くことはできません__。この後で説明する方法で __Python のプログラムから開く__ ことができます。

pickle の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/pickle.html

## `pickle` によるデータの読み込み

pickle で保存したデータを __ファイルから読み込む__ ためには、下記のプログラムのように、`pickle` モジュールの __`load`__ という関数を利用します。

```python
pickle.load(file)
```

実引数 __`file`__ には、`open` で __読込込みモード__ と __バイナリモード__ の両方を表す __`"rb"`__ を指定して開いた __ファイルオブジェクト__ を記述する必要があります。


ファイルから __読み込んだデータ__ は、__`load` の返り値__ として返ります。

下記は、先程 `[1, 2, 3]` を保存した test.pkl からデータを読み込んで表示するプログラムです。実行結果のように、保存した `[1, 2, 3]` が読み込まれたことが確認できます。

```python
with open("test.pkl", "rb") as f:
    print(pickle.load(f)) 
```

実行結果

```
[1, 2, 3]
```

## `pickle` を利用する際の注意点

`pickle` は便利ですが、その反面 __正しく利用__ しないと下記のような __危険性があります__。

:::note alert
__pickle__ で __保存したデータ__ には、__悪意のあるプログラムを混入__ することが可能です。__由来がわからない__ ファイルを __`pickle.load` で開く__ のは __非常に危険__ です。__自分で作成したファイル__ など、__由来が確かなファイルのみを開く__ ようにして下さい。

なお、これはインターネットから入手した __得体のしれないファイルを不用意に開くと危険__ であるのと __同じこと__ です。
:::

## 保存、開くボタンの作成と配置

`pickle` でのデータの保存、読込の方法がわかったので、それらの __操作を行うための GUI__ を考えることにします。本記事では、__保存__、__開く__ の __2 つのボタン__ を、乱数の種のウィジェットの右に配置して〇×ゲームのデータの読み書きを行うことにします。

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

* __6、7 行目__：開く、保存ボタンを作成する

```python
1  def create_widgets(self):
元と同じなので省略
2      self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
3                                     layout=widgets.Layout(width="100px"))   
4
5      # 読み書きのボタンを作成する
6      self.load_button = self.create_button("開く", 100)
7      self.save_button = self.create_button("保存", 100)
元と同じなので省略 
8    
9  Marubatsu_GUI.create_widgets = create_widgets
```

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

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

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

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

```diff_python
def create_widgets(self):
元と同じなので省略
    self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
                                   layout=widgets.Layout(width="100px"))   

    # 読み書きのボタンを作成する
+   self.load_button = self.create_button("開く", 100)
+   self.save_button = self.create_button("保存", 100)
元と同じなので省略 
    
Marubatsu_GUI.create_widgets = create_widgets
```
</details>

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

* __3 行目__：開く、保存ボタンを乱数の種のウィジェットの右に配置する

```python
1  def display_widgets(self):
元と同じなので省略
2      # 乱数の種のウィジェット、読み書きのボタンを横に配置した HBox を作成する
3      hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button])
元と同じなので省略
4
5  Marubatsu_GUI.display_widgets = display_widgets
```

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

```python
def display_widgets(self):
    # 乱数の種のウィジェット、読み書きのボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox3 を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3])) 
    
Marubatsu_GUI.display_widgets = display_widgets
```
</details>

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

```diff_python
def display_widgets(self):
元と同じなので省略
    # 乱数の種のウィジェット、読み書きのボタンを横に配置した HBox を作成する
-   hbox1 = widgets.HBox([self.checkbox, self.inttext])
+   hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button])
元と同じなので省略

Marubatsu_GUI.display_widgets = display_widgets
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行すると、実行結果のように、ファイルの読み書きに関するボタンが上部に表示されるようになります。

```python
gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/9d584c4a-8cad-17db-8617-2a26b8046149.png" width="200">

## ファイルダイアログの表示

__ファイルを操作__ するための __パネル__ のことを __ファイルダイアログ__ と呼びます。ファイルダイアログは、組み込みモジュールである __tkinter__ モジュールの __`filedialog`__ を利用することで表示することができます。ただし、__JupyterLab 上__ で __`filedialog`__ を利用する際には、いくつかの処理を記述する必要があります。

`filedialog` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/dialog.html#module-tkinter.filedialog

:::note
tkinter は、python で __ウィンドウを表示__ し、その __ウィンドウの中に GUI を表示__ したアプリケーションを作成することができる モジュールです。ただし、本記事では __GUI の表示__ をウィンドウを表示するのではなく、__JupyterLab のセルの中__ で行うので tkinter の中の、ファイルダイアログを表示する機能のみを利用しています。
:::

### ファイルを開くダイアログの表示

下記は、ファイルを開くためのファイルダイアログを表示するプログラムです。下記のプログラムの __2 ~ 4 行目__ は、JupyterLab 上でファイルダイアログを利用するために必要な処理[^8]ですが、その意味を理解するためには tkinter の知識が必要になる（正直に言うと、筆者も完全には理解していません）ため本記事では説明は省略します。

6 行目の __`filedialog.askopenfilename`__ が、実行結果のような __ファイルを開く__ ための __ダイアログを表示__ する処理を行います。このメソッドはファイルを開いた場合に、実行結果のように __返り値__ として __開いたファイルのパス__ が返されます。また、ファイルダイアログで __キャンセルボタンをクリック__ した場合は、__空文字__（`""`）が返ります。

なお、実行結果のファイルのパスは、`marubatsu.ipynb` を開いた場合のものです。

```python
1  from tkinter import Tk, filedialog
2
3  root = Tk()
4  root.withdraw()
5  root.call('wm', 'attributes', '.', '-topmost', True)
6
7  print(filedialog.askopenfilename())
```

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

```python
from tkinter import Tk, filedialog

root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)

print(filedialog.askopenfilename())
```
</details>

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1651c79d-01c2-37a2-2e37-2b89f7f339bc.png" width="300">

```
C:/Users/ys/ai/marubatsu/086/marubatsu.ipynb
```

__`filedialog.askopenfilename()`__ が行う処理は、__選択して開いたファイルのパスを返す処理だけ__ で、ファイルを実際に __開く処理は行いません__。ファイルを __開く処理__ は、先程説明した __`open`__ で __別途記述する必要がある__ 点を注意して下さい。

### ファイルを保存するダイアログの表示

下記は、ファイルを保存するためのファイルダイアログを表示するプログラムです。先程との違いは、5 行目が __`filedialog.asksaveasfilename()`__ になった点と、実行結果のように表示されるパネルが __ファイルを保存するためのものに変わった__ だけです。

なお、実行結果のファイルのパスは、__ファイル名に test.pkl を入力した場合__ のものです。

```python
1  root = Tk()
2  root.withdraw()
3  root.call('wm', 'attributes', '.', '-topmost', True)
4
5  print(filedialog.asksaveasfilename())
```

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

```python
root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)

print(filedialog.asksaveasfilename())
```
</details>

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

```diff_python
root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)

-print(filedialog.askopenfilename())
+print(filedialog.asksaveasfilename())
```
</details>

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0973fdc1-0d7c-ff40-a83e-2620f4f6c887.png" width="300">

```
C:/Users/ys/ai/marubatsu/086/test.pkl
```

__`filedialog.asksaveasfilename()`__ が行う処理は、__ファイル名に記述したファイルのパスを返す処理だけ__ で、ファイルを実際に __保存する処理は行いません__。ファイルを __保存する処理__ は、先程説明した __`open`__ で __別途記述する必要がある__ 点を注意して下さい。

## 対戦結果の読み書きの処理の実装

まず、__保存の処理__ について考えることにします。そのためには __どのようなデータを保存する必要があるか__ について考える必要があるので、少し考えてみて下さい。

保存する必要があるデータは以下の通りです。

* 〇×ゲームの __対戦の経過の情報__ が記録されている __`records`__ 属性
* ゲーム盤に表示されている局面の __手数の情報__ を表す __`move_count`__  属性
* それぞれの __手番の担当__ を表す __`ai`__ 属性[^9]

これらのデータをファイルに保存する方法は、それぞれの __属性の名前__ を __キー__ として持つ __dict を作成__ し、その dict をファイルに保存するのが最も簡単でしょう。

__読み込みの処理__ では、ファイルに記録されたデータを読み込んで `records`、`move_count`、`ai` 属性に代入しますが、それだけでは保存した時のゲーム盤の局面は表示されません。その後でどのような処理をすれば良いかについて少し考えてみて下さい。

データを読み込んだ際には、`records` 属性の値を利用して `move_count` の手数の局面を再現する必要がありますが、その処理は `change_step` で行うことができます。

従って、読み書きの処理は、`create_event_handler` を下記のプログラムのように修正することで実装できます。

* __11 ~ 21 行目__：開くボタンのイベントハンドラの定義
* __12 ~ 15 行目__：ファイルを開くダイアログを表示する
* __16 行目__：キャンセルボタンが押された場合は、`askopenfilename` の返り値が空文字になるので、そうでないことを判定する。__この判定を行わない__ と、__キャンセルボタンをクリック__ した際に、ファイルを開けないことが原因で __エラーが発生__ する
* __17 ~ 21 行目__：ファイルダイアログから得られたファイルのパスのファイルを開き、`pickle.load` でデータを読み込み、読み込んだデータを `records`、`ai` 属性に代入し、`change_step` で記録されていた手数の局面に移動する。なお、`move_count` 属性の値は、`change_step` 内で設定されるので、読み込んだデータの代入を行う必要はない
* __23 ~ 35 行目__：保存ボタンのイベントハンドラの定義
* __24 ~ 27 行目__：ファイルの保存ダイアログを表示する
* __28 ~ 35 行目__：ファイルダイアログから得られたファイルのパスのファイルを開き、`records`、`move_count`、`ai` というキーにそれぞれの属性の値を代入する dict を作成し、`pickle` でそのデータをファイルに保存する
* __37、38 行目__：それぞれのボタンにイベントハンドラを結び付ける

```python
 1  import math
 2
 3  def create_event_handler(self):
 4      # 乱数の種のチェックボックスのイベントハンドラを定義する
 5      def on_checkbox_changed(changed):
 6         self.update_widgets_status()
 7       
 8      self.checkbox.observe(on_checkbox_changed, names="value")
 9
10      # 開く、保存ボタンのイベントハンドラを定義する
11      def on_load_button_clicked(b):
12          root = Tk()
13          root.withdraw()
14          root.call('wm', 'attributes', '.', '-topmost', True)        
15          path = filedialog.askopenfilename()
16          if path != "":
17              with open(path, "rb") as f:
18                  data = pickle.load(f)
19                  self.mb.records = data["records"]
20                  self.mb.ai = data["ai"]
21                  change_step(data["move_count"])
22
23      def on_save_button_clicked(b):
24          root = Tk()
25          root.withdraw()
26          root.call('wm', 'attributes', '.', '-topmost', True)        
27          path = filedialog.asksaveasfilename()
28          if path != "":
29              with open(path, "wb") as f:
30                  data = {
31                      "records": self.mb.records,
32                      "move_count": self.mb.move_count,
33                      "ai": self.mb.ai,
34                  }
35                  pickle.dump(data, f)
36            
37      self.load_button.on_click(on_load_button_clicked)
38      self.save_button.on_click(on_save_button_clicked)
元と同じなので省略
39    
40  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):
        root = Tk()
        root.withdraw()
        root.call('wm', 'attributes', '.', '-topmost', True)        
        path = filedialog.askopenfilename()
        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"])
 
    def on_save_button_clicked(b):
        root = Tk()
        root.withdraw()
        root.call('wm', 'attributes', '.', '-topmost', True)        
        path = filedialog.asksaveasfilename()
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                }
                pickle.dump(data, f)
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_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)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        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.draw_board()
        
    # イベントハンドラをボタンに結びつける
    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.draw_board()        

    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)
            self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    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,            
        }
        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_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    # 開く、保存ボタンのイベントハンドラを定義する
+   def on_load_button_clicked(b):
+       root = Tk()
+       root.withdraw()
+       root.call('wm', 'attributes', '.', '-topmost', True)        
+       path = filedialog.askopenfilename()
+       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"])
 
+   def on_save_button_clicked(b):
+       root = Tk()
+       root.withdraw()
+       root.call('wm', 'attributes', '.', '-topmost', True)        
+       path = filedialog.asksaveasfilename()
+       if path != "":
+           with open(path, "wb") as f:
+               data = {
+                   "records": self.mb.records,
+                   "move_count": self.mb.move_count,
+                   "ai": self.mb.ai,
+               }
+               pickle.dump(data, f)
            
+   self.load_button.on_click(on_load_button_clicked)
+   self.save_button.on_click(on_save_button_clicked)
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

実行結果は省略しますが、上記の修正後に下記のプログラムで `gui_play` を実行し、下記の作業を行って、保存したファイルが正しく読み込めることを確認して下さい。

```python 
gui_play()
```

:::note alert
下記の手順 2 で、保存のファイルダイアログで __保存するファイルの名前を入力する際__ には、一度ファイルダイアログの __ファイル名を入力するテキストボックスをクリックしてから__ ファイル名を入力して下さい。そうしないと、JupyterLab のセルに対する操作が行われてしまう（例えば a を押すと JupyterLab の現在のセルの前に 新しいセルが挿入されてしまう）からです。

この問題については、次回の記事で対応する予定です。
:::


1. 着手を行ってゲームの決着をつける
2. 保存ボタンをクリックし、好きなフォルダに save.pkl という名前で保存する
3. 手順 5 で __AI の担当も再現されることを確認できるようにする__ ために、Dropdown で現在の担当とは __別の AI を選択__ してリセットボタンをクリックする
4. 開くボタンをクリックし、手順 2 で保存したファイルを開く
5. 手順 2 で保存した局面が再現される。その際に、手番を担当する AI も再現される
6. リプレイ機能のボタンをクリックして、途中経過も正しく再現されていることを確認する

これで対戦の結果をファイルに保存することができるようになりましたが、現状のプログラムで表示される __ファイルダイアログ__ には、上記の赤いノートで説明した以外にも、__いくつか不便な点がある__ ので、次回の記事ではそれらの問題を解決することにします。どのような問題があるかについて考えてみて下さい。

# 今回の記事のまとめ

今回の記事では、乱数の種の GUI と、対戦結果のファイルへの読み書きを実装しました。

すみません、今回で GUI の実装を終える予定だったのですが終わりませんでした。次回の記事で終えられるように頑張りたいと思います。

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

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

https://github.com/ysgeso/marubatsu/blob/master/086/marubatsu.ipynb

以下のリンクは、今回の記事で更新した marubatsu.py です。

https://github.com/ysgeso/marubatsu/blob/master/086/marubatsu_new.py

以下のリンクは、今回の記事で更新した util.py です。

https://github.com/ysgeso/marubatsu/blob/master/086/util_new.py

# 次回の記事

https://qiita.com/ysgeso/items/5941cc59c3e0e173c5cf

[^1]:IntText には、整数以外の文字を入力しようとしても入力することはできません
[^2]:忘れている方がいるかもしれませんので補足しますが、1 行目で、ai モジュールを `ai_module` という名前でインポートしているのは、__`gui_play`__ が __`ai` という仮引数を持つ__ ため、`ai` モジュールと __名前が被るため__ です
[^3]:本記事では行いませんが、ゲームの途中で乱数の種の処理を行いたい場合は、イベントハンドラを記述する必要があります
[^4]:他にも多数の実引数を記述できます。詳細は[こちらのリンク先](https://docs.python.org/ja/3/library/functions.html#open)を参照して下さい
[^5]:他にもいくつかのモードがあります。詳細は[こちらのリンク先](https://docs.python.org/ja/3/library/functions.html#open)を参照して下さい
[^6]:__文字列型以外__ の形式で保存されたデータの事を __バイナリデータ__ と呼びます。
[^7]:`pickle` でデータを保存したファイルの拡張子には `.pkl` が良く使われます
[^8]:この方法は、[こちらのページ](https://stackoverflow.com/questions/42307442/in-jupyter-notebook-how-to-bring-tkinter-filedialog-window-to-the-top-of-all-win)でみつけた方法です
[^9]:この属性の保存は忘れやすいのではないかと思います。実際に筆者も最初は忘れました