目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
play
メソッドのバグの修正
今回の記事を記述する際に気が付いたのですが、play
メソッドには バグ があります。それは、下記 のプログラムのように、キーワード引数 gui
を 記述せず に play
メソッドを呼び出して CUI で 〇×ゲーム を 遊ぼうとした場合 に、実行結果 のように Dropdown が表示 され、さらに エラーが発生 する点です。これらの原因について少し考えてみて下さい。
from marubatsu import Marubatsu
from ai import ai1s
mb = Marubatsu()
mb.play(ai=[ai1s, ai1s])
実行結果(下図は、画像なので操作することはできません)
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
Cell In[1], line 5
2 from ai import ai1s
4 mb = Marubatsu()
----> 5 mb.play(ai=[ai1s, ai1s])
File c:\Users\ys\ai\marubatsu\077\marubatsu.py:382, in Marubatsu.play(self, ai, ai_dict, params, verbose, seed, gui, size)
379 fig.canvas.mpl_connect("button_press_event", on_mouse_down)
381 self.restart()
--> 382 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
UnboundLocalError: cannot access local variable 'ax' where it is not associated with a value
上記のエラーメッセージは、以下のような意味を持ちます。
-
UnboundLocalError
束縛されていない(unbound)ローカル(local)な名前に関するエラー -
cannot access local variable 'ax' where it is not associated with a value
値(value)が関連付けられていない(associated with)ax というローカル変数(local variable)にアクセス(access)することができない(cannot)
エラーの原因の検証
変数 に 値を代入 することを、束縛 する(bound)、または 値 を 関連付ける などと呼びます。従って、この エラーメッセージ は、ax
という ローカル変数 に 値 が 代入されていない という 意味 を 表します。そこで、play
メソッドの 中 で、ax
に 値を代入 する 処理 を 探してみる と、下記 の部分で 行われている ことがわかります。
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
fig, ax = plt.subplots(figsize=[size, size])
上記 のプログラムの if gui:
から、ax
は、gui
に True
が代入 されている 場合のみ で 値が代入される ことが わかります。先程 のように、キーワード引数 gui
を 記述せず に play
メソッドを 呼び出す と、gui
には デフォルト値 である False
が 代入 されるので、エラーメッセージが示すように、ax
に 値 が 代入されていない ことが 確認 できました。
エラーの修正方法
このエラーは、play_loop
を 呼び出す際 に、ax
に 値 が 代入されていない ことが 原因 なので、gui
が False
の場合 に、ax
に 何らかの値 を 代入 することで 修正 できます。そこで、ax
に どのような値 を 代入すればよいか について 検討 するために、play_loop
の 中 で、ax
が どのように利用 されているかについて 調べる ことにします。
play_loop
の 中 で ax
に 関する処理 が 行われる のは、下記 の 4、6 行目 の部分です。
略
1 if gui:
2 # AI どうしの対戦の場合は画面を描画しない
3 if ai[0] is None or ai[1] is None:
4 self.draw_board(ax)
略
5 if gui:
6 self.draw_board(ax)
略
いずれ も、gui
に True
が 代入 されている 場合のみ で 実行される処理 なので、gui
に False
が 代入 されている場合は、play_loop
の 中 では ax
は 利用されない ことが わかります。そのため、gui
に False
が 代入 されている場合は、ax
に 何を代入 しても かまわない ことが わかりました。そこで、本記事 では 下記 のプログラムのように、gui
に False
が代入 されていた場合は、ax
に None
を代入 することにします。
-
8、9 行目:
gui
がTrue
でない、すなわちFalse
の場合 にax
にNone
を 代入する。この処理 を 10 行目より前 に 実行 することで、play_loop
を 呼び出す際 に、ax
に 値 が 必ず代入される ようになる
1 import matplotlib.pyplot as plt
2 import ipywidgets as widgets
3 import math
4
5 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
6 # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
7 if gui:
元と同じなので省略
8 else:
9 ax = None
元と同じなので省略
10 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
11
12 Marubatsu.play = play
行番号のないプログラム
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)
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 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 を作成する
dropdown_circle = widgets.Dropdown(
options=ai_dict,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_dict,
description="×",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[1],
)
# リセットボタンを作成する
button = widgets.Button(
description="リセット",
layout=widgets.Layout(width="100px"),
)
# 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
display(hbox)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
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)
# 次の手番の処理を行うメソッドを呼び出す
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
修正箇所
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):
元と同じなので省略
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
元と同じなので省略
+ else:
+ ax = None
元と同じなので省略
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムを 実行 すると、エラー は 発生しなくなります が、実行結果 のように CUI で〇×ゲームを遊んでいるにも 関わらず 、最初に Dropdown が表示 されてしまうという 問題 は 解消されていない ことが わかります。
mb.play(ai=[ai1s, ai1s])
実行結果(下図は、画像なので操作することはできません)
Turn o
...
...
...
以下略(ai1s どうしの対戦が表示される)
CUI での Dropdown の表示の修正
gui
に False
が代入 されている場合でも Dropdown が表示 されるのは、play
メソッド内で、Dropdown の処理 が gui
に 代入 されている 値 に 関わらず、必ず実行 されるように 記述されている からです。下記 のプログラムのように、それらの処理 を if gui:
の ブロックの中 に 移動 することで、この問題 を 解決 することが できます。なお、下記 のプログラムは、gui
の ブロックの中 に 移動 した プログラムのみ を示します。
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 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 を作成する
dropdown_circle = widgets.Dropdown(
options=ai_dict,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_dict,
description="×",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[1],
)
# リセットボタンを作成する
button = widgets.Button(
description="リセット",
layout=widgets.Layout(width="100px"),
)
# 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
display(hbox)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
プログラム全体
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:
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 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 を作成する
dropdown_circle = widgets.Dropdown(
options=ai_dict,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_dict,
description="×",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[1],
)
# リセットボタンを作成する
button = widgets.Button(
description="リセット",
layout=widgets.Layout(width="100px"),
)
# 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
display(hbox)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
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)
# 次の手番の処理を行うメソッドを呼び出す
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
上記 の 修正後 に、下記 のプログラムを 実行 すると、実行結果 のように Dropdown は 表示されなく なり、CUI で 〇×ゲーム が 実行される ようになります。
mb.play(ai=[ai1s, ai1s])
実行結果
Turn o
...
...
...
以下略(ai1s どうしの対戦が表示される)
バグを見逃した原因
今回のバグ を これまで 見逃し ていた 原因 は、GUI で 〇×ゲーム を 遊べるよう に play
メソッドを 修正した際 に、CUI での play
メソッドの 処理 が 正しく動作するか を 確認しなかった ためです。このように、プログラムで 何かを修正 した場合は、それまで に 行えていた処理 が 正しく動作するか を 確認 することを 怠らない ことが 重要 です。
マジックコマンドを Python で実行する方法
これまで のプログラムでは、play
メソッドの 実引数 に gui=True
を 記述 して 〇×ゲーム を GUI で 遊ぶ際 に、%matplotlib widget
という マジックコマンド を 実行する必要 が ありました。以前の記事で、マジックコマンド は、Python の プログラムではない ので、marubatsu.py に 記述 すると エラーが発生 するため、JupyterLab 上 で 実行 する 必要がある と説明しましたが、下記 のプログラムで、%matplotlib widget
の マジックコマンド を Python の プログラム で 実行できます。
get_ipython().run_line_magic('matplotlib', 'widget')
%matplotlib widget
以外 の マジックコマンド も 同様の方法 で 実行できます。
get_ipython
は、IPython モジュールで 定義 されていますが、インポートしなくても利用できる ようです。get_ipython
を 実行 した際に エラーが発生 した場合は、下記 のプログラムを 実行 して インポート して下さい。
from IPython import get_ipython
そこで、下記 のプログラムの 5 行目 のように、gui
に True
が 代入 されている 場合 に 上記 の 処理を行う ように play
メソッドを 修正 することにします。
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
3 if gui:
4 # %matplotlib widget のマジックコマンドを実行する
5 get_ipython().run_line_magic('matplotlib', 'widget')
元と同じなので省略
6
7 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')
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 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 を作成する
dropdown_circle = widgets.Dropdown(
options=ai_dict,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_dict,
description="×",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[1],
)
# リセットボタンを作成する
button = widgets.Button(
description="リセット",
layout=widgets.Layout(width="100px"),
)
# 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
display(hbox)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
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)
# 次の手番の処理を行うメソッドを呼び出す
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):
元と同じなので省略
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
+ get_ipython().run_line_magic('matplotlib', 'widget')
元と同じなので省略
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 すると、実行結果 のように、%matplotlib widget
を 実行しなくても GUI で 〇×ゲームを遊べる ことが 確認できます。
from util import gui_play
gui_play()
実行結果(下図は、画像なので操作することはできません)
Dropdown による AI の選択
これまで に 作成 した play
メソッドは、手番を担当 する AI を選択する ための Dropdown の 表示 を 行うだけ で、Dropdown の 選択を変更 しても 何の処理 も 行われません。そこで、Dropdown の 選択を変更 することで、手番を担当 する AI を 変更する ようにします。
なお、実際 には、Dropdown で 人間を選択 することも できます が、毎回「Dropdown で AI または人間 を 選択 する」のように 表記 するのは 冗長 なので、以後 は、人間の選択 を 省略 して、「Dropdown で AI を選択 する」のように 表記 します。
Dropdown の選択の変更を反映させるタイミング
まず、Dropdown の 選択の変更 を どの時点 で 〇×ゲーム に 反映させるか を 決める必要 が あります。候補 としては 下記 の 3 つ が挙げられるでしょう。
- Dropdown が 変更された時点 で 即座 に 変更を反映 する
- ゲーム を リセットした時点 で 変更を反映 し、新しい対戦を開始する
- Dropdown の 変更を適用 する ボタンを用意 し、押された時 に 変更を反映 する
上記 の どれを採用 しても 構いません が、1 の「Dropdown が 変更された時点 で 即座 に 変更を反映 する」という 方法 は、Dropdown の 操作ミス によって 意図しない変更 が行われてしまう 可能性がある ので、本記事 では 2 と 3 の 方法 を 採用 することにします。
ゲームのリセットによる Dropdown の反映
以前の記事で説明したように、Dropdown が 選択中 の 項目の値 は、Dropdown の value
属性 に 代入 されています。そのため、下記 のプログラムのように、リセットボタン の イベントハンドラ である on_button_clicked
を 修正 することで、ボタン を クリックした時 に、Dropdown に 選択 されている AI で 新しい対戦 が 開始される ようになります。
-
5 行目:〇 の担当 を表す Dropdown の
value
属性が"人間"
の場合は、ai[0]
に 人間の担当 を表すNone
を代入 する。それ以外 の場合は、value
属性に AI の 関数オブジェクト が 代入されている ので、value
属性の 値 をai[0]
に そのまま代入 する - 6 行目:上記 と 同様の処理 を × の担当 を表す Dropdown に対して 行う
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_button_clicked(b):
4 self.restart()
5 ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
6 ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
7 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
8
9 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')
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 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 を作成する
dropdown_circle = widgets.Dropdown(
options=ai_dict,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_dict,
description="×",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[1],
)
# リセットボタンを作成する
button = widgets.Button(
description="リセット",
layout=widgets.Layout(width="100px"),
)
# 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
display(hbox)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
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)
# 次の手番の処理を行うメソッドを呼び出す
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_button_clicked(b):
self.restart()
+ ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
+ ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行した後 で、Dropdown を 変更 してから リセットボタン を 押す と、変更した AI で 新しい対戦 が 開始 されます。実行結果 は 省略 しますが、実際 に Dropdown に 様々な組み合わせ を 選択 して 確認 して下さい。
gui_play()
変更ボタンによる Dropdown の反映
AI の担当 を 変更するボタン が 行う処理 は、ゲーム を リセットする処理 を 除けば、リセットボタン が 行う処理 と 同じ です。そのため、その処理 は 下記 のプログラムのように 記述できます。
なお、ボタン が 2 つ に 増えた ので、それぞれ の ボタン の ウィジェット を 代入する変数 の 名前 を、change_button
、reset_button
のようにして 区別できる ように しました。ボタン を クリック した場合の イベントハンドラ の 名前 も 同様 です。
- 3 ~ 6 行目:変更ボタン の ウィジェット を 作成 する
-
9 行目:リセットボタン の ウィジェット を 代入 する 変数の名前 を
reset_button
に 変更 する - 15 行目:HBox に 変更ボタン の ウィジェット を 追加 する
- 19 ~ 22 行目:変更ボタン を クリック した際の イベントハンドラ を 定義 する
-
25 行目:リセットボタン を クリック した際の イベントハンドラ の 名前 を
on_reset_button_clicked
に 変更 する - 32、33 行目:それぞれの ボタン をそれぞれの イベントハンドラ に 結びつける
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # 変更ボタンを作成する
3 change_button = widgets.Button(
4 description="変更",
5 layout=widgets.Layout(width="100px"),
6 )
7
8 # リセットボタンを作成する
9 reset_button = widgets.Button(
10 description="リセット",
11 layout=widgets.Layout(width="100px"),
12 )
13
14 # 〇 と × の dropdown と ボタンを横に配置した HBox を作成し、表示する
15 hbox = widgets.HBox([dropdown_circle, dropdown_cross, change_button, reset_button])
16 display(hbox)
17
18 # 変更ボタンのイベントハンドラを定義する
19 def on_change_button_clicked(b):
20 ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
21 ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
22 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
23
24 # リセットボタンのイベントハンドラを定義する
25 def on_reset_button_clicked(b):
26 self.restart()
27 ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
28 ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
29 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
30
31 # イベントハンドラをボタンに結びつける
32 change_button.on_click(on_change_button_clicked)
33 reset_button.on_click(on_reset_button_clicked)
元と同じなので省略
34
35 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')
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 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 を作成する
dropdown_circle = widgets.Dropdown(
options=ai_dict,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_dict,
description="×",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[1],
)
# 変更ボタンを作成する
change_button = widgets.Button(
description="変更",
layout=widgets.Layout(width="100px"),
)
# リセットボタンを作成する
reset_button = widgets.Button(
description="リセット",
layout=widgets.Layout(width="100px"),
)
# 〇 と × の dropdown と ボタンを横に配置した HBox を作成し、表示する
hbox = widgets.HBox([dropdown_circle, dropdown_cross, change_button, reset_button])
display(hbox)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
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)
# 次の手番の処理を行うメソッドを呼び出す
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):
元と同じなので省略
# 変更ボタンを作成する
+ change_button = widgets.Button(
+ description="変更",
+ layout=widgets.Layout(width="100px"),
+ )
# リセットボタンを作成する
- button = widgets.Button(
+ reset_button = widgets.Button(
description="リセット",
layout=widgets.Layout(width="100px"),
)
# 〇 と × の dropdown と ボタンを横に配置した HBox を作成し、表示する
- hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
+ hbox = widgets.HBox([dropdown_circle, dropdown_cross, change_button, reset_button])
display(hbox)
# 変更ボタンのイベントハンドラを定義する
+ def on_change_button_clicked(b):
+ ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
+ ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
+ self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
- def on_button_clicked(b):
+ def on_reset_button_clicked(b):
self.restart()
ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# イベントハンドラをボタンに結びつける
+ change_button.on_click(on_change_button_clicked)
- button.on_click(on_button_clicked)
+ reset_button.on_click(on_reset_button_clicked)
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 すると、実行結果 のように、変更ボタン が 表示される ようになります。また、Dropdown を 変更 してから 変更ボタン を 押す と、ゲーム が リセットされず に、AI の 担当のみ が 変更 されます。実行結果 は 省略 しますが、実際 に Dropdown に 様々な組み合わせ を 選択 して 確認 して下さい。
gui_play()
実行結果(下図は、画像なので操作することはできません)
変更ボタンのイベントハンドラに関する注意点
変更ボタン の イベントハンドラ の 処理 は、それぞれの 手番を担当 する AI を変更 する 処理 なので、下記 のプログラムの 5 行目 の self_loop
を 呼び出す処理 は 必要ない のではないかと 思った人 は いないでしょうか?しかし、下記の 5 行目 を 削除 すると バグが発生 します。どのようなバグが発生するかについて少し考えてみて下さい。
1 # 変更ボタンのイベントハンドラを定義する
2 def on_change_button_clicked(b):
3 ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
4 ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
5 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
確かに、上記の 5 行目 を 削除しても それぞれの 手番 を 担当する AI は 変化 しますが、人間の手番 の時に、人間から AI に変更 しても、変更した AI が 着手 を 行わない という 問題が発生 します。その 理由 は上記の 5 行目 が、現在の手番 が AI の場合 に 着手 を 選択する処理 を 行っている からです。なお、この問題の 原因 は、以前の記事で説明した、リセットボタン の クリック後 に AI が 着手を行わない 問題と 同じ です。
play
メソッドの改良
これで、play
メソッドの 実行後 に、手番 を 担当する AI を Dropdown で 変更できる ようになりましたが、play
メソッドにはいくつか 改良 できる 余地がある ので、それらの 改良を行う ことにします。改良案についていくつか考えてみて下さい。
Dropdown に関する処理の改良
これまで の play
メソッドでは、Dropdown に関する 処理 を、2 つ の Dropdown ごと に 記述 してきましたが、この 2 つ の Dropdown に 関する処理 は ほとんど同じ処理 なので、for 文 を使って 下記 のプログラムのように まとめる ことが できます。
-
3 行目:2 つ の Dropdown を 代入する変数
drop_list
を 空の list で 初期化 する - 7 ~ 16 行目:2 つ の Dropdown を 作成する処理 を、for 文 の ブロック内 に 記述 する
-
7 行目:Dropdown の 左に表示 する 文字列 を、
i
の値 に 応じて計算 する -
8 ~ 16 行目:Dropdown を 作成 し、
dropdown_list
の 要素 に 追加 する -
11 行目:キーワード引数
description
に 7 行目 で 計算 した 文字列を指定 する -
14 行目:キーワード引数
value
にselect_values[i]
を 指定 する -
18 行目:HBox に
dropdown_list
の 各要素 を 記述 するように 修正 する -
23、24、30、31 行目:
ai
の 各要素 に Dropdown の 選択項目の値 を 代入 する 処理 を、繰り返し処理 を使って 行う ように 修正 する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # 〇 と × の Dropdown を格納する list
3 dropdown_list = []
4 # ai に代入されている内容を ai_dict に追加する
5 for i in range(2):
元と同じなので省略
6 # Dropdown の description を計算する
7 description = "〇" if i == 0 else "×"
8 dropdown_list.append(
9 widgets.Dropdown(
10 options=ai_dict,
11 description=description,
12 layout=widgets.Layout(width="100px"),
13 style={"description_width": "20px"},
14 value=select_values[i],
15 )
16 )
元と同じなので省略
17 # 〇 と × の dropdown とボタンを横に配置した HBox を作成し、表示する
18 hbox = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
19 display(hbox)
20
21 # 変更ボタンのイベントハンドラを定義する
22 def on_change_button_clicked(b):
23 for i in range(2):
24 ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
25 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
26
27 # リセットボタンのイベントハンドラを定義する
28 def on_reset_button_clicked(b):
29 self.restart()
30 for i in range(2):
31 ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
32 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui) 元と同じなので省略
33
34 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')
# それぞれの手番の担当を表す 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"),
)
# 〇 と × の 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()
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)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
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)
# 次の手番の処理を行うメソッドを呼び出す
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):
元と同じなので省略
# 〇 と × の Dropdown を格納する list
+ dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
元と同じなので省略
# 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],
+ )
+ )
# 〇 と × の Dropdown を作成する
- dropdown_circle = widgets.Dropdown(
- options=ai_dict,
- description="〇",
- layout=widgets.Layout(width="100px"),
- style={"description_width": "20px"},
- value=select_values[0],
- )
- dropdown_cross = widgets.Dropdown(
- options=ai_dict,
- description="×",
- layout=widgets.Layout(width="100px"),
- style={"description_width": "20px"},
- value=select_values[1],
- )
元と同じなので省略
# 〇 と × の dropdown とボタンを横に配置した HBox を作成し、表示する
- hbox = widgets.HBox([dropdown_circle, dropdown_cross, change_button, reset_button])
+ hbox = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
display(hbox)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
- ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
- ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
+ 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()
- ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value
- ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value
+ 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) 元と同じなので省略
Marubatsu.play = play
実行結果 は 先程と同じ なので 省略 しますが、上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 すると、正しい処理 が行われることが 確認 できます。
gui_play()
変更ボタンとリセットボタンの処理の統合
変更ボタン と リセットボタン が 行う処理 の 違い は、ゲームのリセット を 行うか どうか だけ です。そのため、リセットボタン の イベントハンドラ を 下記 のプログラムのように 修正 して 統合 することが できます。
-
5 行目:ゲームのリセット の 後の処理 は 変更ボタン の イベントハンドラ の 処理と同じ なので、
on_change_button_clicked
を 呼び出す ように 修正 する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_reset_button_clicked(b):
4 self.restart()
5 on_change_button_clicked(b)
元と同じなので省略
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')
# それぞれの手番の担当を表す 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"),
)
# 〇 と × の 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)
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)
# 次の手番の処理を行うメソッドを呼び出す
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_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
元と同じなので省略
Marubatsu.play = play
実行結果 は 先程と同じ なので 省略 しますが、上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 すると、正しい処理 が行われることが 確認 できます。
gui_play()
担当する AI の表示
AI を 選択 する Dropbox は、選択する項目 を 変更 しても、変更ボタン または リセットボタン を 押さない限り、ゲーム に 反映 されることは ありません。そのため、Dropbox に 選択 されている 項目 と、手番 を 担当する AI が 異なる場合 が あります。
その状況 は、混乱の原因 となる 可能性がある ので、ゲーム盤 の 下 に、例えば「人間 VS ai1s」のような、実際 に行われている 対戦カード を 表示する ようにします。どのようにプログラムを修正すれば良いかについて少し考えてみて下さい。
対戦カード を 表示 するためには、play
メソッドの 仮引数 ai
の 情報 が 必要 になりますが、ゲーム盤 を 描画 する draw_board
メソッドには その情報 が 渡されていない ので、そのまま では 対戦カード を 表示 することは できません。そこで、下記 のプログラムのように、draw_board
メソッドに、手番 を 担当する AI の 情報を代入 する 仮引数ai
を 追加 し、その情報 を使って ゲーム盤の下 に 対戦カード を 表示 するようにします。
- 4 行目:それぞれの 手番の担当 の 文字列 を 代入する変数 を 空の list で 初期化 する
-
5、6 行目:繰り返し処理 を使って、それぞれの 手番の担当 を表す 文字列 を 計算 し、
names
の 要素に追加 する - 7 行目:ゲーム盤 の 下部の座標 を 指定 して、対戦カード を 表示 する。なお、この座標 は 試行錯誤 して 調整したもの である
1 def draw_board(self, ax, ai):
元と同じなので省略
2 # 上部のメッセージを描画する
3 # 対戦カードの文字列を計算する
4 names = []
5 for i in range(2):
6 names.append("人間" if ai[i] is None else ai[i].__name__)
7 ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
元と同じなので省略
8
9 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
names = []
for i in range(2):
names.append("人間" if ai[i] is None else ai[i].__name__)
ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
+ names = []
+ for i in range(2):
+ names.append("人間" if ai[i] is None else ai[i].__name__)
+ ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
元と同じなので省略
Marubatsu.draw_board = draw_board
次に、draw_board
を 呼び出し ている、play
メソッドと play_loop
メソッドを 修正 します。下記 は、修正 した play
メソッドで、9 行目 で、draw_board
に 実引数 ai
を 追加 するという 修正 を行っています。
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # ローカル関数としてイベントハンドラを定義する
3 def on_mouse_down(event):
4 # Axes の上でマウスを押していた場合のみ処理を行う
5 if event.inaxes and self.status == Marubatsu.PLAYING:
6 x = math.floor(event.xdata)
7 y = math.floor(event.ydata)
8 self.move(x, y)
9 self.draw_board(ax, ai)
10
11 # 次の手番の処理を行うメソッドを呼び出す
12 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
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')
# それぞれの手番の担当を表す 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"),
)
# 〇 と × の 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)
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_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)
+ self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
Marubatsu.play = play
下記は、play_loop
の 修正 で、修正箇所 は 5、7 行目 です。
1 def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
2 if gui:
3 # AI どうしの対戦の場合は画面を描画しない
4 if ai[0] is None or ai[1] is None:
5 self.draw_board(ax, ai)
元と同じなので省略
6 if gui:
7 self.draw_board(ax, ai)
元と同じなので省略
8
9 Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self, ai, ax, params, verbose, gui):
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲーム盤の表示
if verbose:
if gui:
# AI どうしの対戦の場合は画面を描画しない
if ai[0] is None or ai[1] is None:
self.draw_board(ax, ai)
# 手番を人間が担当する場合は、play メソッドを終了する
if ai[index] is None:
return
else:
print(self)
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self, **params[index])
else:
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
if coord == "exit":
print("ゲームを終了します")
return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
# (x, y) に着手を行う
try:
self.move(int(x), int(y))
except:
print("整数の座標を入力して下さい")
# 決着がついたので、ゲーム盤を表示する
if verbose:
if gui:
self.draw_board(ax, ai)
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
if gui:
# AI どうしの対戦の場合は画面を描画しない
if ai[0] is None or ai[1] is None:
- self.draw_board(ax)
+ self.draw_board(ax, ai)
元と同じなので省略
if gui:
- self.draw_board(ax)
+ self.draw_board(ax, ai)
元と同じなので省略
Marubatsu.play_loop = play_loop
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 すると、実行結果 のように ゲーム盤の下 に 対戦カード が 表示される ようになります。また、Dropdown を 変更 して 変更ボタン や リセットボタン を 押す と、対戦カード が 更新される ことを 確認 して下さい。
gui_play()
実行結果(下図は、画像なので操作することはできません)
ゲームが決着した際の表示の工夫
現状 では、ゲーム が 決着しているか どうかが わかりづらい ので、ゲーム が 決着した場合 は、ゲームの画像 の 背景色 を 変更 することにします。Figure の 背景色 は、下記 のプログラムのように、Figure の set_facecolor
メソッドで 変更 できます。また、Axes が 登録 されている Figure は、Axes の figure
属性に 代入 されています。
本記事 では、ゲーム の 決着がついた場合 に 背景色 を 薄い黄色 を表す "lightyellow" にしますが、他の色が良い と思った人は 自由に変更 して下さい。
- 6 行目:ゲームの決着 が ついていない 場合は 白 を、決着が ついている 場合は 薄い黄色 を表す 文字列を計算 する
-
7 行目:
ax
が 登録 されている Figure の 背景色 を 6 行目 で 計算した色 に 変更 する
1 def draw_board(self, ax, ai):
元と同じなので省略
2 # 枠と目盛りを表示しないようにする
3 ax.axis("off")
4
5 # ゲームの決着がついていた場合は背景色を変える
6 facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
7 ax.figure.set_facecolor(facecolor)
元と同じなので省略
8
9 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を変える
facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
names = []
for i in range(2):
names.append("人間" if ai[i] is None else ai[i].__name__)
ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を変える
+ facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
+ ax.figure.set_facecolor(facecolor)
元と同じなので省略
Marubatsu.draw_board = draw_board
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 し、ゲーム の 決着をつける と、実行結果 のように ゲームの画像 の 背景色 が 薄い黄色 になります。
gui_play()
実行結果(下図は、画像なので操作することはできません)
引き分けの表示の変更
現状 では、引き分け になった場合に "winner draw" という メッセージ が 表示 されますが、勝者(winner)が draw という メッセージ は 変な気がする ので、下記 のプログラムのように、"Draw game" と 表示 するように 修正 します。あと、細かい修正 ですが、違和感があった ので、勝者 の メッセージ の "winner" の 頭文字 を 大文字に修正 しました。
- 6、7 行目:引き分け の場合の メッセージ を "Draw game" にする
- 10 行目:勝者 の メッセージ の "winner" の 頭文字 を 大文字に修正 する
1 def draw_board(self, ax, ai):
元と同じなので省略
2 # ゲームの決着がついていない場合は、手番を表示する
3 if self.status == Marubatsu.PLAYING:
4 text = "Turn " + self.turn
5 # 引き分けの場合
6 elif self.status == Marubatsu.DRAW:
7 text = "Draw game"
8 # 決着がついていれば勝者を表示する
9 else:
10 text = "Winner " + self.status
11 ax.text(0, -0.2, text, fontsize=20)
元と同じなので省略
12
13 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
names = []
for i in range(2):
names.append("人間" if ai[i] is None else ai[i].__name__)
ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 引き分けの場合
elif self.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 引き分けの場合
+ elif self.status == Marubatsu.DRAW:
+ text = "Draw game"
# 決着がついていれば勝者を表示する
else:
- text = "winner " + self.status
+ text = "Winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
元と同じなので省略
Marubatsu.draw_board = draw_board
上記 の 修正後 に、下記 のプログラムで gui_play
を 実行 し、引き分け にすると、実行結果 のように "Draw game" が 表示 されるようになります。実行結果は省略しますが、どちらかが勝利 した 場合 は、先頭 に "Winner" が表示 されることも 確認 できます。
gui_play()
実行結果(下図は、画像なので操作することはできません)
CUI の場合の処理の確認
最後に、下記 のプログラムを 実行 し、CUI で 〇×ゲーム を 遊んだ場合 に 正しい処理 が行われることを 確認 します。なお、実行結果は省略します。
mb.play(ai=[ai1s, ai1s])
今回の記事のまとめ
今回の記事は、以下の内容を行いました。
-
play
メソッドの バグを修正 した -
%matplotlib widget
の マジックコマンド を プログラムで実行 できるようにした - Dropdown で 選択した AI で 対戦が行われる ようにした
-
play
メソッドに関する いくつかの改良 を行った
本記事で紹介した以外の play
メソッドの 改良 を 思いついた人 は、余裕があれば その 改良 を 行ってみて下さい。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
なお、play
、play_loop
、draw_board
の docstring の 説明 が 微妙に間違っていた ので 修正 しました。ただし、過去の記事 を 修正 するのは 大変すぎる ので、過去の記事 の docstring は そのまま にしてあります。
今回の記事では、util.py は変更していません。
次回の記事