目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの 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 行目:それまでの
hbox1
とhbox2
を、hbox2
とhbox3
に修正する - 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
属性のTrue
とFalse
を反転 する処理なので、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
を使ったファイルの処理は下記のプログラムのように記述します。下記のプログラムでは、f
に open
の返り値である ファイルオブジェクトが代入 され、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 をファイルに保存するのが最も簡単でしょう。
読み込みの処理 では、ファイルに記録されたデータを読み込んで 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 行目:それぞれのボタンにイベントハンドラを結び付ける
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 の現在のセルの前に 新しいセルが挿入されてしまう)からです。
この問題については、次回の記事で対応する予定です。
- 着手を行ってゲームの決着をつける
- 保存ボタンをクリックし、好きなフォルダに save.pkl という名前で保存する
- 手順 5 で AI の担当も再現されることを確認できるようにする ために、Dropdown で現在の担当とは 別の AI を選択 してリセットボタンをクリックする
- 開くボタンをクリックし、手順 2 で保存したファイルを開く
- 手順 2 で保存した局面が再現される。その際に、手番を担当する AI も再現される
- リプレイ機能のボタンをクリックして、途中経過も正しく再現されていることを確認する
これで対戦の結果をファイルに保存することができるようになりましたが、現状のプログラムで表示される ファイルダイアログ には、上記の赤いノートで説明した以外にも、いくつか不便な点がある ので、次回の記事ではそれらの問題を解決することにします。どのような問題があるかについて考えてみて下さい。
今回の記事のまとめ
今回の記事では、乱数の種の GUI と、対戦結果のファイルへの読み書きを実装しました。
すみません、今回で GUI の実装を終える予定だったのですが終わりませんでした。次回の記事で終えられるように頑張りたいと思います。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
以下のリンクは、今回の記事で更新した util.py です。
次回の記事
-
IntText には、整数以外の文字を入力しようとしても入力することはできません ↩
-
忘れている方がいるかもしれませんので補足しますが、1 行目で、ai モジュールを
ai_module
という名前でインポートしているのは、gui_play
がai
という仮引数を持つ ため、ai
モジュールと 名前が被るため です ↩ -
本記事では行いませんが、ゲームの途中で乱数の種の処理を行いたい場合は、イベントハンドラを記述する必要があります ↩
-
文字列型以外 の形式で保存されたデータの事を バイナリデータ と呼びます。 ↩
-
pickle
でデータを保存したファイルの拡張子には.pkl
が良く使われます ↩ -
この属性の保存は忘れやすいのではないかと思います。実際に筆者も最初は忘れました ↩