目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
リプレイ機能
これまで のプログラムでは、play
メソッドで AI どうし の 対戦 を GUI で 行った場合 に、AI が すべての着手 を 行った後 の、決着がついた ゲーム盤が 表示 されていました。今回の記事では、対戦 の 途中経過 を 表示 する、リプレイ機能 を 実装 することにします。
リプレイ機能 を 実装 するためには、リプレイを行うための、具体的な機能 について 検討 する必要があります。どのような機能 が 必要か について少し考えてみて下さい。
リプレイに必要な機能
ぱっと思いつく リプレイ に 必要な機能 には、下記 のような機能があるでしょう。他にも必要な機能を思いついた方は、実装してみて下さい。
- ゲーム開始時 まで 戻す 機能
- 1 手戻す 機能
- 1 手進める 機能
- ゲームの終了時 まで 進める 機能
なお、実装を進めていけばすぐに気づくことができると思いますが、リプレイ機能 は、ゲームの決着 が ついていない場合 も行えた方が 便利 です。そこで、上記の 最後の機能 を「最後 に 行われた着手 まで 進める 機能」に 変更 することにします。
次に、それぞれの機能 を呼び出すための UI を 決める必要 が あります。それについても少し考えてみて下さい。
リプレイ機能の UI の設定と実装
最も簡単 な UI としては、それぞれの機能 をリセットボタンなどと同様に、ボタンで呼び出す というものでしょう。本記事 でも ボタンの UI を 採用 することにします。
次に、ボタン に 表示 する 文字列 と、ボタンの配置 を 考える必要 が あります。それらについても少し考えてみて下さい。
ボタンの表記と配置例
本記事では、それぞれの ボタン に 表示 する 文字列 を、動画プレーヤーなど で良く見かける 表記 に 倣って、下記 のような 文字列 とし、4 つのボタン を 横に並べて配置 することにします。また、4 つのボタン は、これまでに表示 していた、Dropdown や リセットボタン の 下に配置 することにします。他の表記が良いと思った人は自由に変更してください。
機能 | 表記 |
---|---|
ゲーム開始時まで戻す | << |
1 手戻す | < |
1 手進める | > |
最後に行われた着手まで進める | >> |
リプレイ機能のウィジェットの作成と配置
上記の 4 つのボタン の ウィジェット は、リセットボタン と 同様の方法 で 作成 することが できます。その際に、それぞれ の ウィジェット を 代入 する 変数の名前 を 下記 の表のように 名づける ことにします。なお、prev_button
の prev は、「前の」という 意味 を表す previous の略 です。
機能 | 表記 | 変数名 |
---|---|---|
ゲーム開始時まで戻す | << | first_button |
1 手戻す | < | prev_button |
1 手進める | > | next_button |
最後に行われた着手まで進める | >> | last_button |
play
メソッドの修正
上記の 4 つのボタン の ウィジェット は、Dropbox や リセットボタン を 横に並べて配置 するために利用した、HBox を使って 横に配置 します。また、Dropbox など の HBox と リプレイのボタン の HBox は、以前の記事で説明したように、VBox を使って 縦に並べて配置 することが できます。
下記 は、リプレイ機能 の ボタン を 作成 して 配置 するように play
メソッドを 修正 したプログラムです。
- ウィジェット を 配置 して 表示 する 処理 を 一か所にまとめる ために、7 行目の下 にあった HBox を 作成 して 表示 する 処理 を 43 行目 以降に 移動 する
- 25 ~ 40 行目:リプレイ機能 の 4 つのボタン を 作成 し、変数に代入 する
- 43 行目:Dropdown と リセットボタン を 横に並べて配置 する HBox を 作成 する
- 45 行目:リプレイ機能 の ボタン を 横に並べて配置 する HBox を 作成 する
- 47 行目:2 つ の HBox を 縦に並べて配置 する VBox を 作成 し、表示 する
1 from marubatsu import Marubatsu
2 import matplotlib.pyplot as plt
3 import ipywidgets as widgets
4 import math
5
6 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3): # 変更ボタンのイベントハンドラを定義する
元と同じなので省略
7 # この下にあった hbox を作成するプログラムを 43 行目に移動する
8
9 def on_change_button_clicked(b):
10 for i in range(2):
11 ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
12 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
13
14 # リセットボタンのイベントハンドラを定義する
15 def on_reset_button_clicked(b):
16 self.restart()
17 on_change_button_clicked(b)
18
19 # イベントハンドラをボタンに結びつける
20 change_button.on_click(on_change_button_clicked)
21 reset_button.on_click(on_reset_button_clicked)
22
23 # 2 行目の UI を作成する
24 # リプレイのボタンを作成する
25 first_button = widgets.Button(
26 description="<<",
27 layout=widgets.Layout(width="100px"),
28 )
29 prev_button = widgets.Button(
30 description="<",
31 layout=widgets.Layout(width="100px"),
32 )
33 next_button = widgets.Button(
34 description=">",
35 layout=widgets.Layout(width="100px"),
36 )
37 last_button = widgets.Button(
38 description=">>",
39 layout=widgets.Layout(width="100px"),
40 )
41
42 # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
43 hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
44 # リプレイ機能のボタンを横に配置した HBox を作成する
45 hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
46 # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
47 display(widgets.VBox([hbox1, hbox2]))
元と同じなので省略
48
49 Marubatsu.play = play
行番号のないプログラム
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# 変更ボタンを作成する
change_button = widgets.Button(
description="変更",
layout=widgets.Layout(width="100px"),
)
# リセットボタンを作成する
reset_button = widgets.Button(
description="リセット",
layout=widgets.Layout(width="100px"),
)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = widgets.Button(
description="<<",
layout=widgets.Layout(width="100px"),
)
prev_button = widgets.Button(
description="<",
layout=widgets.Layout(width="100px"),
)
next_button = widgets.Button(
description=">",
layout=widgets.Layout(width="100px"),
)
last_button = widgets.Button(
description=">>",
layout=widgets.Layout(width="100px"),
)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
- # 〇 と × の dropdown とボタンを横に配置した HBox を作成し、表示する
- hbox = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
- display(hbox)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
+ first_button = widgets.Button(
+ description="<<",
+ layout=widgets.Layout(width="100px"),
+ )
+ prev_button = widgets.Button(
+ description="<",
+ layout=widgets.Layout(width="100px"),
+ )
+ next_button = widgets.Button(
+ description=">",
+ layout=widgets.Layout(width="100px"),
+ )
+ last_button = widgets.Button(
+ description=">>",
+ layout=widgets.Layout(width="100px"),
+ )
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
+ hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
+ hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
+ display(widgets.VBox([hbox1, hbox2]))
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 すると、実行結果 のように、Dropdown の下 に リプレイ機能 の 4 つのボタン が 並んで表示 されるようになります。
from util import gui_play
gui_play()
実行結果(下図は、画像なので操作することはできません)
ボタンを作成する関数の定義
上記 のプログラムでは、ボタン を 作成するたび に 下記 のような 複数行 のプログラムを 記述する必要 があり、記述が面倒、プログラム が 長くなる などの 問題 が あります。
first_button = widgets.Button(
description="<<",
layout=widgets.Layout(width="100px"),
)
ボタンごと の 違い は、キーワード引数 description
の 値だけ です。そこで、ボタン を 作成する処理 を行う 下記 のような 関数を定義 することにします。なお、ボタンの横幅 を 設定できる と 便利 なので、横幅 も 実引数 で 設定できる ようにしました。
名前:ボタンを作成する ので create_button
とする
処理:指定 した 表記 と 横幅 の ボタン の ウィジェット を 作成 する
入力:仮引数 description
に ボタン の 表示内容 を、width
に ボタン の 横幅 を 代入 する
出力:作成 した ウィジェット
この関数 を Marubatsu
クラスの メソッド として 定義 する事は 可能 ですが、本記事 では 下記の理由 から play
メソッドの ローカル関数 として 定義 する事にします。
-
仮引数
self
を 利用しない -
この処理 は
play
メソッドの 中でしか利用しない
play
メソッド 以外の場所 で 利用 する 可能性がある 場合は、Marubatsu
クラスの 静的メソッド として 定義しても良い でしょう。
他にも、通常の関数 として 定義 し、util.py に 保存 するという方法もあります。
下記は、create_button
の 定義を追加 して 修正 した play
メソッドです。
-
3 ~ 7 行目:
create_button
をplay
メソッドの ローカル関数 として 定義 する -
10、11、14 ~ 17 行目:
create_button
を 利用 して ボタンを作成 するように 修正 する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # ボタンを作成するローカル関数を定義する
3 def create_button(description, width):
4 return widgets.Button(
5 description=description,
6 layout=widgets.Layout(width=f"{width}px"),
7 )
8
9 # 変更、リセットボタンを作成する
10 change_button = create_button("変更", 100)
11 reset_button = create_button("リセット", 100)
元と同じなので省略
12 # 2 行目の UI を作成する
13 # リプレイのボタンを作成する
14 first_button = create_button("<<", 100)
15 prev_button = create_button("<", 100)
16 next_button = create_button(">", 100)
17 last_button = create_button(">>", 100)
元と同じなので省略
Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
元と同じなので省略
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
元と同じなので省略
Marubatsu.play = play
実行結果は同じなので省略しますが、上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 すると 正しい処理 が 行われる ことが 確認 できます。
gui_play()
リプレイ機能の実装
UI が 表示される ようになったので、次は それぞれのボタン を クリックした際に実行 される イベントハンドラ を 定義 する 必要 が あります。イベントハンドラ に どのような処理 を 記述 すれば良いかについて少し考えてみて下さい。
リプレイ機能が行う処理
リプレイ機能 では、4 種類 の ボタン を押すことで、ゲーム が 開始され てから 終了するまで の 任意の手数 の ゲーム盤 を 表示 します。そのため、リプレイ機能 を 実装 するためには、任意の手数 が 指定 された際に、その手数 の ゲーム盤を表示 するという 処理 を 行う必要 が あります。そこで、そのような処理 を行う 下記 の 関数を定義 する事にします。
名前:表示 する ゲーム の 手数(step)を 変更(change)するので change_step
とする
処理:指定 した 手数 の ゲーム盤 の 画像を描画 する
入力:仮引数 step
に 手数 を 代入 する
出力:なし
change_step
を どのように実装 すればよいかについて少し考えてみて下さい。
ゲーム盤のデータを記録する方法
任意の手数 の ゲーム盤 を 描画 するためには、その手数 の ゲーム盤 を 再現 する 必要 があります。その方法 の 一つ に、着手 を 行うたび に、ゲーム盤 を表す borad
属性の データ を コピー して 記録しておく という方法があり、以下 のような 方法 で 実装 できます。
- 各手数 の ゲーム盤の情報 を 記録 するための 属性の名前 を 考える
-
ゲーム を リセット する 処理 を行う
restart
メソッドの 中 で、その属性 に 空の list を 代入 して 初期化 する -
着手 を行う
move
メソッドの 中 で、その属性 の 要素 に、着手後 のboard
属性の 内容 をdeepcopy(self.board)
で 深いコピー を行ったものを 追加 する
ゲーム盤 の データ を 記録する際 は、代入や浅いコピーではなく、深いコピー を 行わなければならない 点に 注意 して下さい。
代入 では うまくいかない理由 は、list に 要素を追加 する際に行われる 代入 が オブジェクト を 共有 するという 処理 だからです。着手 を行うと board
属性に 代入 された list の 要素の値 は 変化 しますが、board
属性に 代入 される オブジェクト は 変化しません。そのため、着手 を 行うたび に list に board
属性を 追加 すると、すべての要素 に 同一のオブジェクト が 代入される ことになります。
浅いコピー では うまくいかない理由 は、board
属性の 要素の値 が ミュータブル なデータである list だからです。忘れた方は、以前の記事を 復習 して下さい。
任意の手数 の ゲーム盤 を 再現 する 他の方法 は、今後の記事 で 説明します。
record_boards
属性の実装
下記 は、属性の名前 を record_boards
という 名前 にした場合の 実装例 です。それぞれ の メソッド の 最後の行 が 追加 した プログラム です。プログラムを見ればわかると思いますが、行っている処理 は、着手を記録 する records
属性の 処理 と 同様 です。
from copy import deepcopy
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = []
self.board_records = []
Marubatsu.restart= restart
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
self.records.append(self.last_move)
self.board_records.append(deepcopy(self.board))
Marubatsu.move = move
修正箇所
from copy import deepcopy
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = []
+ self.board_records = []
Marubatsu.restart= restart
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
self.records.append(self.last_move)
+ self.board_records.append(deepcopy(self.board))
Marubatsu.move = move
上記 のプログラムで、records
属性に 追加 する self.last_move
を 深いコピー で コピーするべき ではないかと 思った方 が いるかもしれない ので 補足 します。
self.last_move
の 値 は、直前 の self.last_move = x, y
で、新しい tuple が 代入 されるので、コピーを行わなくても records
属性の 他の要素 と 値が共有される ことは ありません。それに対し、self.board
は 着手 を行っても 同じオブジェクト を 参照し続ける ので、深いコピー を 行わず に board_records
に追加 すると、board_records
の 全ての要素 に 同じオブジェクト が 代入される ことになります。
上記 のプログラムでは、着手 を記録する 属性 と、ゲーム盤 を記録する 属性 を 別々の属性 にしましたが、下記 のプログラムのように、tuple や dict を使って、records
属性 に まとめて記録 することもできます。なお、下記 のプログラムは、move
メソッド内で、記録を行う処理 の 部分 です。
# tuple を利用する場合
self.records.append((self.last_move, deepcopy(self.board)))
# dict を利用する場合
self.records.append({
"move": self.last_move,
"board": deepcopy(self.board))
})
ただし、上記 のように records
の 要素 に 代入 する データの形式 を 変更 すると、records
属性を 利用する これまでの プログラム が 動作しなくなる ので、本記事 では 上記の方法 は 採用しない ことにします。
change_step
の実装
下記は、change_step
の 定義 です。
-
3 行目:
step
手目の ゲーム盤 の データ をboard_records
から 取り出し、深いコピー を行ったデータをboard
に代入 する -
5 行目:
draw_board
を使って、ゲーム盤の描画 を 更新 する
1 def change_step(step):
2 # step 手目のゲーム盤のデータをコピーし、board に代入する
3 self.board = deepcopy(self.board_records[step])
4 # 描画を更新する
5 self.draw_board(ax, ai)
3 行目 を self.board = self.board_records[step]
のように データ を コピーせず に 代入する と データ が 共有される ので バグが発生 します。具体的 には、着手 を行って self.board
の 要素の値 が 変化 すると self.board_records[step]
の 要素の値 も 同時 に 変更 されてしまいます。
次に、この関数 を どこに記述する かを 考える必要 が あります。この関数 の 中 では、self
を 利用 しているので、Marubatsu
クラスの メソッド として 定義 する方法が考えられますが、その場合は、ax
、ai
を 仮引数 とする 必要がある 点が少し 面倒 です。一方、play
メソッドの ローカル関数 として 定義 すれば、それらの名前 を そのまま利用 することが できる ので、本記事では play
メソッドの ローカル関数 として 定義 します。
ax
や ai
を、play
メソッドの ローカル変数 として ではなく、self.ax
や self.ai
のように、Marubatsu
クラスの 属性にする ことで、change_step
を Marubatsu
クラスの メソッドとして定義 しても 仮引数 ax
や ai
を 記述しなくても済む ようになります。ただし、今からそのようにプログラムを修正すると 大幅な修正 が 必要になる ので 本記事 では 採用しない ことにします。
下記 は、そのように play
メソッドを 修正 したプログラムです。
-
9 ~ 13 行目:
change_step
の 定義 を 記述 する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # 2 行目の UI を作成する
3 # リプレイのボタンを作成する
4 first_button = create_button("<<", 100)
5 prev_button = create_button("<", 100)
6 next_button = create_button(">", 100)
7 last_button = create_button(">>", 100)
8
9 def change_step(step):
10 # step 手目のゲーム盤のデータをコピーし、board に代入する
11 self.board = self.board_records[step]
12 # 描画を更新する
13 self.draw_board(ax, ai)
元と同じなので省略
14
15 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
def change_step(step):
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 描画を更新する
self.draw_board(ax, ai)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
+ def change_step(step):
+ # step 手目のゲーム盤のデータをコピーし、board に代入する
+ self.board = self.board_records[step]
+ # 描画を更新する
+ self.draw_board(ax, ai)
元と同じなので省略
Marubatsu.play = play
ゲーム開始時まで戻すボタンのイベントハンドラの定義
ゲーム開始まで戻す ボタンの イベントハンドラ で 行う処理 は、1 手 も 着手 を 行っていない、0 手目 の ゲーム盤の画像 を 描画する処理 なので、change_turn(0)
によって 行う ことが できます。そこで、その イベントハンドラ を 下記 のように on_first_button_clicked
という 名前 で 定義 し、ボタン に 結びつける ことにします。
- 8、9 行目:イベントハンドラ を 定義 する
- 11 行目:ボタン に イベントハンドラ を 結び付ける
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 def change_step(step):
3 # step 手目のゲーム盤のデータを board に代入する
4 self.board = self.board_records[step]
5 # 描画を更新する
6 self.draw_board(ax, ai)
7
8 def on_first_button_clicked(b):
9 change_step(0)
10
11 first_button.on_click(on_first_button_clicked)
元と同じなので省略
12
13 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
def change_step(step):
# step 手目のゲーム盤のデータを board に代入する
self.board = self.board_records[step]
# 描画を更新する
self.draw_board(ax, ai)
def on_first_button_clicked(b):
change_step(0)
first_button.on_click(on_first_button_clicked)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
def change_step(step):
# step 手目のゲーム盤のデータを board に代入する
self.board = self.board_records[step]
# 描画を更新する
self.draw_board(ax, ai)
+ def on_first_button_clicked(b):
+ change_step(0)
+ first_button.on_click(on_first_button_clicked)
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 し、いくつかの着手 を 行った後 で、<< ボタンを クリック すると、実行結果 のように、1 手目 の 着手 が 行われた後 の ゲーム盤 が 描画 されるという 問題が発生 します。
gui_play()
実行結果(下図は、1 手目に真ん中のマスに着手を行った場合の図です)
問題の原因の検証
on_first_button_clicked
が 行う処理 は、change_step(0)
だけ なので、問題の原因 は、change_step(0)
が 行う処理 を 調べる ことで 検証 できます。change_step(0)
では、self.board = self.board_records[0]
を 実行 するので、self.board_records[0]
が 1 手目 の 着手 を 行った後 の ゲーム盤のデータ であることが 推測 されます。
board_record
属性に対して 行われる処理 は、以下の通りです。
- ゲーム の リセット時 に、空の list で 初期化 する
- 着手 を 行うたび に、着手後 の ゲーム盤のデータ の 深いコピー を 要素 に 追加 する
上記 から、ゲーム開始時 の board_records
属性の 値 は 空の list なので、1 手目 の ゲーム盤のデータ は、board_records
の 0 番 の 要素 に 代入 されることが わかります。
このことから、この問題 の 原因 は ゲーム開始時 の ゲーム盤のデータ が board_records
属性の 0 番 の 要素 に 代入されていない ことが 原因 であることが わかります。
そのことは、ゲームの開始直後 に << ボタンを クリック した際に、下記 のような エラーが発生 することからも 確認 できます。下記 のエラーは、空の要素 が 代入 されている board_records
の 0 番 の 要素 を 参照 しようとしたために 発生 します。
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[7], line 91
90 def on_first_button_clicked(b):
---> 91 change_step(0)
Cell In[7], line 86
84 def change_step(step):
85 # step 手目のゲーム盤のデータを board に代入する
---> 86 self.board = self.board_records[step]
87 # 描画を更新する
88 self.draw_board(ax, ai)
IndexError: list index out of range
バグの修正
従って、下記 のプログラムの 9 行目 のように、restart
メソッド内で、board_records
の 0 番 の 要素 に、ゲーム開始時 の ゲーム盤のデータ を コピーしたもの を 代入 するようにすることで この問題 を 解決 できます。
1 def restart(self):
2 self.initialize_board()
3 self.turn = Marubatsu.CIRCLE
4 self.move_count = 0
5 self.status = Marubatsu.PLAYING
6 self.last_move = -1, -1
7 self.last_turn = None
8 self.records = []
9 self.board_records = [deepcopy(self.board)]
10
11 Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = []
self.board_records = [deepcopy(self.board)]
Marubatsu.restart = restart
修正箇所
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = []
- self.board_records = []
+ self.board_records = [deepcopy(self.board)]
Marubatsu.restart = restart
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 し、いくつかの着手 を 行った後 で、<< ボタンを クリック すると、実行結果 のように、ゲーム開始直後 の ゲーム盤 が 描画 されます。また、ゲームの開始直後 に << ボタンを クリック しても エラー が 発生しなくなる ことが 確認 できます。
gui_play()
実行結果(下図は、画像なので操作することはできません)
<< ボタンの 処理 には いくつかの問題 があります。そのうちの 1 つ は、上図 に 現れています。どのような問題があるかについて少し考えてみて下さい。
<< ボタンの処理の問題点 その 1
問題 の 1 つ は、上図 のように、ゲーム開始時 の 手番 が × に なる場合がある という点です。ただし、<< ボタンを クリック した時に、手番 が 〇 になる 場合も あります。そこで、<< ボタンを クリック した時に、どのような場合 に × の手番 に なるか を 色々と試して調べて みて下さい。
<< ボタンの処理の問題点 その 2
2 つ目の問題点は、ゲームの決着 が ついた場合 に、<< ボタンを クリック すると、下図 のように 決着 が ついた状態 で ゲーム開始時 の ゲーム盤 が 表示される点 です。この問題の原因について少し考えてみて下さい。
<< ボタンの処理の問題点 その 3
着手 を行い << ボタンを クリック するという 作業 を 繰り返す と、下図 のように すべてのマス が 埋まっていない のに 引き分け になるという 問題が発生 します。これが 3 つ目 の 問題 です。この問題についても原因を少し考えてみて下さい。
なお、この後 で << ボタンを クリック すると、引き分け の 状態 で ゲーム開始時 の ゲーム盤 が 表示される という、2 つ目 の 問題 が 発生 します。正しい状態 で ゲーム を 開始 したい場合は、リセットボタン を クリック して下さい。
1 つ目と 2 つ目の問題の原因の検証と修正
試行錯誤 を 行う ことで、<< ボタンを クリック すると、以下の状態で ゲーム開始時 の ゲーム盤 が 表示される ことがわかります。
- ゲームの決着 が ついていない 場合は、<< ボタンを クリック した 時点の手番 になる
- ゲームの決着 が ついている 場合は、決着 が ついたまま の 状態 になる
手番 の情報は turn
属性に、ゲームの決着がついているか どうかは status
属性に 代入 されていますが、change_step
内 では、その いずれ の 属性の値 も 変化しません。そのため、1 つ目 と 2 つ目 の 問題が発生 します。
そこで、下記 のプログラムのように、change_step
の中で、turn
属性と status
属性の 値を計算 するという 処理 を 記述 することで、1 つ目 と 2 つ目 の 問題 を 解決 できます。
-
6 行目:〇×ゲームは、偶数 手目が 〇 の手番 なので、手数 を表す
step
が 偶数の場合 は 〇、そうでない場合 は × を表す データ をturn
属性に 代入 する。偶数かどうか は 余り を 計算 する % 演算子 と、2 で 割った余り が 0 であるか どうかで 判定 できる -
8 行目:
judge
メソッドを使ってstatus
属性の 値 を 計算し直す
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 def change_step(step):
3 # step 手目のゲーム盤のデータをコピーし、board に代入する
4 self.board = deepcopy(self.board_records[step])
5 # 手番を計算する。step が偶数の場合は 〇 の 手番
6 self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
7 # status 属性を judget を使って計算する
8 self.status = self.judge()
9 # 描画を更新する
10 self.draw_board(ax, ai)
元と同じなので省略
11
12 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
def change_step(step):
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手番を計算する。step が偶数の場合は 〇 の 手番
self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
self.status = self.judge()
# 描画を更新する
self.draw_board(ax, ai)
def on_first_button_clicked(b):
change_step(0)
first_button.on_click(on_first_button_clicked)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
def change_step(step):
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手番を計算する。step が偶数の場合は 〇 の 手番
+ self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
+ self.status = self.judge()
# 描画を更新する
self.draw_board(ax, ai)
元と同じなので省略
Marubatsu.play = play
実行結果 は 省略 しますが、上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 し、1 つ目 と 2 つ目 の 問題 が 解決された ことを 確認 して下さい。
gui_play()
3 つ目の問題の原因の検証
さまざまな 試行錯誤 を 繰り返す ことで わかるようになる と思いますが、この現象 は << ボタンを クリック して ゲーム開始時 の 局面 に 戻した後 で 発生 する現象です。リセット ボタンを クリック してゲームをリセットした 場合 には 発生しません。
この問題は、引き分け に 関する現象 なので、引き分け の 判定方法 を 調べる必要 が あります。引き分け の 判定 は、judge
メソッド内の 下記 のプログラムで行っています。
elif self.is_full():
return Marubatsu.DRAW
上記 の is_full
は 下記 のように 定義 される メソッド です。
def is_full(self):
return self.move_count == self.BOARD_SIZE ** 2
上記 から、着手 した 回数 を表す move_count
属性が、ゲーム盤 の マスの数 に 等しい場合 に 引き分け と 判定 されることがわかります。そこで、move_count
に 関する処理 を 調べる と、move_count
に対する 処理 は 以下の場合 に 行われる ことが わかります。
-
restart
メソッドで ゲーム を リセットした際 に、0
で初期化 される -
move
メソッドで 着手 が 行われた際 に1
加算 される
従って、<< ボタンで ゲーム開始時 の 局面 を 表示 した場合は、move_count
属性の 値 は 変化しません。例えば、6 手目 まで 着手 を 行った場合 に << ボタンを クリック すると、ゲーム盤の表示 は ゲーム開始時 の 表示 に 戻ります が、着手 を 行った回数 を表す move_count
属性の 値 は 6
のまま です。そのため、その後 で 3 回着手 を行うと、move_count
の値が 9
になり、下図 のように 引き分け になってしまいます。
問題の原因がわかったので、この 問題 を 解決する方法 を少し考えてみて下さい。
3 つ目の問題の修正
この問題は、<< ボタンを クリック した時に、 move_count
に 0
を 代入する ことで 解決 できます。ただし、change_step
は 0 手目だけでなく、任意の手数 に移動する処理を行うので、move_count
には 0
ではなく、下記 のプログラムの 6 行目 のように、手数 を表す 仮引数 step
をそのまま 代入 することで この問題 を 解決 することが できます。
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 def change_step(step):
3 # step 手目のゲーム盤のデータをコピーし、board に代入する
4 self.board = deepcopy(self.board_records[step])
5 # 手数を表す step を move_count に代入する
6 self.move_count = step
7 # 手番を計算する。step が偶数の場合は 〇 の 手番
8 self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
9 # status 属性を judget を使って計算する
10 self.status = self.judge()
11 # 描画を更新する
12 self.draw_board(ax, ai)
元と同じなので省略
13
14 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
def change_step(step):
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手数を表す step を move_count に代入する
self.move_count = step
# 手番を計算する。step が偶数の場合は 〇 の 手番
self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
self.status = self.judge()
# 描画を更新する
self.draw_board(ax, ai)
def on_first_button_clicked(b):
change_step(0)
first_button.on_click(on_first_button_clicked)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
def change_step(step):
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手数を表す step を move_count に代入する
+ self.move_count = step
# 手番を計算する。step が偶数の場合は 〇 の 手番
self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
self.status = self.judge()
# 描画を更新する
self.draw_board(ax, ai)
元と同じなので省略
Marubatsu.play = play
実行結果 は 省略 しますが、上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 し、1 つ目 と 2 つ目 の 問題 が 解決された ことを 確認 して下さい。
gui_play()
残りボタンのイベントハンドラの定義
1 手戻す 処理と 1 手進む 処理は、現在表示 されている 局面の手数 の 前後 の 手数の局面 を 表示 するという 処理 です。現在の手数 は move_count
に 代入 されているので、その値 を 元 に change_step
メソッドを使って イベントハンドラ を 記述 することが できます。
最後 に 行われた着手 まで 進める という 処理 は、慣れないとどのように記述すれば良いかわからないかもしれません。どこか に 最後 に 行われた着手 が 何手目 であるかを 表すデータ が 存在している ので、それが何か を少し考えてみて下さい。
board_records
には ゲーム開始時 から、最後 に 行われた着手 の局面までの ゲーム盤のデータ が list の要素 に 代入 されています。従って、list の 要素数 を 計算 する 組み込み関数 len
を使って、board_records
の 要素の数 から、最後 に 行われた着手 の 手数 を 計算 することが できます。その際 に、__board_records
__ には 着手 を 行う前 の ゲーム開始時 の 局面のデータ が 代入 されているため、len(board_records) - 1
のように、要素の数 から 1 を引く必要 がある点に 注意 して下さい。
下記は、上記 の イベントハンドラ と、ボタン に イベントハンドラ を 結び付ける 処理を play
メソッドに 記述 したプログラムです。
-
5、6 行目:1 手前 の 手数 を
self.move_count - 1
で 計算 して 表示 する -
8、9 行目:1 手後 の 手数 を
self.move_count + 1
で 計算 して 表示 する -
11、12 行目:最後 に 行われた着手 の 手数 を
len(self.board_records) - 1
で 計算 して 表示 する - 15 ~ 17 行目:それぞれ の ボタン と イベントハンドラ を 結び付ける
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 def on_first_button_clicked(b):
3 change_step(0)
4
5 def on_prev_button_clicked(b):
6 change_step(self.move_count - 1)
7
8 def on_next_button_clicked(b):
9 change_step(self.move_count + 1)
10
11 def on_last_button_clicked(b):
12 change_step(len(self.board_records) - 1)
13
14 first_button.on_click(on_first_button_clicked)
15 prev_button.on_click(on_prev_button_clicked)
16 next_button.on_click(on_next_button_clicked)
17 last_button.on_click(on_last_button_clicked)
元と同じなので省略
18
19 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
def change_step(step):
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手数を表す step を move_count に代入する
self.move_count = step
# 手番を計算する。step が偶数の場合は 〇 の 手番
self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
self.status = self.judge()
# 描画を更新する
self.draw_board(ax, ai)
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
change_step(self.move_count - 1)
def on_next_button_clicked(b):
change_step(self.move_count + 1)
def on_last_button_clicked(b):
change_step(len(self.board_records) - 1)
first_button.on_click(on_first_button_clicked)
prev_button.on_click(on_prev_button_clicked)
next_button.on_click(on_next_button_clicked)
last_button.on_click(on_last_button_clicked)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
def on_first_button_clicked(b):
change_step(0)
+ def on_prev_button_clicked(b):
+ change_step(self.move_count - 1)
+ def on_next_button_clicked(b):
+ change_step(self.move_count + 1)
+ def on_last_button_clicked(b):
+ change_step(len(self.board_records) - 1)
first_button.on_click(on_first_button_clicked)
+ prev_button.on_click(on_prev_button_clicked)
+ next_button.on_click(on_next_button_clicked)
+ last_button.on_click(on_last_button_clicked)
元と同じなので省略
Marubatsu.play = play
実行結果 は 省略 しますが、上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 し、4 つのボタンでリプレイを行うことができることを 確認 して下さい。
gui_play()
今回の記事のまとめ
今回の記事では、リプレイ機能 を行うための 4 つ の ボタンの配置 と、イベントハンドラ の 定義 を行いました。それぞれ の ボタン で 実際 に リプレイ を行うことが できます が、4 つのボタン に対して 様々な操作 を 行う ことで、4 つのボタン が 行う処理 には いくつかの問題がある ことが わかる と思います。次回の記事 では それらの問題 を 解決する方法 を 紹介 するので、余裕がある方は どのような問題があるか について 調べておいてください。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事