1
1

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

Last updated at Posted at 2024-06-02

目次と前回の記事

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

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

ルールベースの AI の一覧

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

乱数の種の設定

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

play メソッドでは、キーワード引数 seed乱数の種設定することができます が、現状では GUI で〇×ゲームを遊ぶ際に、後から乱数の種を設定することはできません。そこで、GUI で 〇× ゲームを遊ぶ際に、後から乱数の種を設定できる ようにすることにします。

乱数の種を設定する GUI

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

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

Checkbox の属性と表示

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

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

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

import ipywidgets as widgets

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

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

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

IntText の属性と表示

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

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

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

display(widgets.IntText())

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

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

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

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

他にも、整数以外の数値を入力できる FloatText、文字列を入力できる 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 を代入する
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__```
行番号のないプログラム
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__
修正箇所
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__

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

  • 6 行目:キーワード引数 seed=seed を追加する
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
行番号のないプログラム
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
修正箇所
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

値が設定された 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 を作成する
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
行番号のないプログラム
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
修正箇所
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

Checkbox と IntText の配置

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

  • 3 行目:乱数の種のウィジェットを横に配置した HBox を作成して hbox1 に代入する
  • 5、7 行目:それまでの hbox1hbox2 を、hbox2hbox3 に修正する
  • 9 行目:3 つの Hbox を縦に配置する
 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
行番号のないプログラム
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
修正箇所
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

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

from util import gui_play

gui_play()

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

gui_play の修正

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

  • 4 行目:デフォルト値を None とする仮引数 seed を追加する
  • 6 行目play メソッドの実引数に、キーワード引数 seed=seed を追加する
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)
行番号のないプログラム
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)
修正箇所
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)

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

gui_play(seed=123)

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

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

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

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

  • 5、6 行目:乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
 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
行番号のないプログラム
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
修正箇所
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

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

from ai import ai14s

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

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

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

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 属性の TrueFalse を反転 する処理なので、not 演算子 を使って行うことができる
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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

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

本記事では行いませんが、薄く表示されていることがわかりづらいと思った方は、ボタンで行ったように、IntText の表示の色を変更するなどの方法が考えられます。

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

問題の原因と修正

この問題は、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 が呼び出されるようにする
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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

対戦の保存と読込

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

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

ファイルのオープン

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

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

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

f = open(file, mode)

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

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

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

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

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

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

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

ファイルのクローズ

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

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

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

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

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

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

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

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

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

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

pickle によるデータの保存

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

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 という名前の ファイルが作成される ので確認して下さい。

import pickle

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

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

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

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

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

pickle.load(file)

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

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

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

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

実行結果

[1, 2, 3]

pickle を利用する際の注意点

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

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

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

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

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

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

  • 6、7 行目:開く、保存ボタンを作成する
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
行番号のないプログラム
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
修正箇所
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

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

  • 3 行目:開く、保存ボタンを乱数の種のウィジェットの右に配置する
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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

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

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

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

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

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

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

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

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

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

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())
行番号のないプログラム
from tkinter import Tk, filedialog

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

print(filedialog.askopenfilename())

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

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

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

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

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

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

1  root = Tk()
2  root.withdraw()
3  root.call('wm', 'attributes', '.', '-topmost', True)
4
5  print(filedialog.asksaveasfilename())
行番号のないプログラム
root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)

print(filedialog.asksaveasfilename())
修正箇所
root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)

-print(filedialog.askopenfilename())
+print(filedialog.asksaveasfilename())

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

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

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

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

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

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

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

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

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

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

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

  • 11 ~ 21 行目:開くボタンのイベントハンドラの定義
  • 12 ~ 15 行目:ファイルを開くダイアログを表示する
  • 16 行目:キャンセルボタンが押された場合は、askopenfilename の返り値が空文字になるので、そうでないことを判定する。この判定を行わない と、キャンセルボタンをクリック した際に、ファイルを開けないことが原因で エラーが発生 する
  • 17 ~ 21 行目:ファイルダイアログから得られたファイルのパスのファイルを開き、pickle.load でデータを読み込み、読み込んだデータを recordsai 属性に代入し、change_step で記録されていた手数の局面に移動する。なお、move_count 属性の値は、change_step 内で設定されるので、読み込んだデータの代入を行う必要はない
  • 23 ~ 35 行目:保存ボタンのイベントハンドラの定義
  • 24 ~ 27 行目:ファイルの保存ダイアログを表示する
  • 28 ~ 35 行目:ファイルダイアログから得られたファイルのパスのファイルを開き、recordsmove_countai というキーにそれぞれの属性の値を代入する dict を作成し、pickle でそのデータをファイルに保存する
  • 37、38 行目:それぞれのボタンにイベントハンドラを結び付ける
 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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

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

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

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

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

今回の記事のまとめ

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

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

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

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

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

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

次回の記事

  1. IntText には、整数以外の文字を入力しようとしても入力することはできません

  2. 忘れている方がいるかもしれませんので補足しますが、1 行目で、ai モジュールを ai_module という名前でインポートしているのは、gui_playai という仮引数を持つ ため、ai モジュールと 名前が被るため です

  3. 本記事では行いませんが、ゲームの途中で乱数の種の処理を行いたい場合は、イベントハンドラを記述する必要があります

  4. 他にも多数の実引数を記述できます。詳細はこちらのリンク先を参照して下さい

  5. 他にもいくつかのモードがあります。詳細はこちらのリンク先を参照して下さい

  6. 文字列型以外 の形式で保存されたデータの事を バイナリデータ と呼びます。

  7. pickle でデータを保存したファイルの拡張子には .pkl が良く使われます

  8. この方法は、こちらのページでみつけた方法です

  9. この属性の保存は忘れやすいのではないかと思います。実際に筆者も最初は忘れました

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1