目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
play
メソッドのバグ
前回の記事で、play
メソッドを 実行 した際に 仮引数 ai
に 代入 されている 値を元 に、Dropdown の項目 を 自動的に設定 するプログラムを 記述 しましたが、その際に play
メソッドには 重大なバグ が いくつか存在 するという説明を行いました。
バグその 1(人間の項目が複数作られる)
下記 は、play
メソッドで 人間 VS AI の 対戦 を行うプログラムです。実行結果 の 下図左 から、正しく動作 しているように 見えるかも しれませんが、Dropdown の上 で マウスを押す と、右下図 のように、人間の項目 が 複数表示 されるという 問題が発生 することが 確認 できます。このようなことが起きる原因について少し考えてみて下さい。
%matplotlib widget
from marubatsu import Marubatsu
mb = Marubatsu()
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
バグの原因の考察
下記は、play
メソッドの中で、Dropdown の項目 を 作成 する部分のプログラムです。
1 # ai に代入されている内容を ai_list に追加する
2 for i in range(2):
3 # ai[i] が ai_values に登録済かどうかを判定する
4 if ai[i] not in ai_values:
5 # ラベルと項目の値を計算する
6 if ai[i] is None:
7 label = "人間"
8 value = "人間"
9 else:
10 label = ai[i].__name__
11 value = ai[i]
12 # 項目を登録する
13 ai_list.append((label, value))
14 ai_values.append(value)
15 select_values.append(value)
下記 は、上記の中 で Dropdown の項目 に 関する処理 を 箇条書き にしたものです。
-
4 行目:
ai[i]
がai_values
に 登録済か どうかを 判定 する -
8 行目:人間の担当 の場合は、
value
にNone
を 代入 する -
11 行目:AI の担当 場合は、
value
にai[i]
を 代入 する -
14 行目:
ai_values
にvalue
を 追加 する
上記 の 4 行目 で ai_values
に 登録済であるか どうかを 判定 する際に使われている ai[i]
と、15 行目 で ai_values
に 追加 する、value
に代入 されている 値 が、人間 が 担当する場合 は 異なる点 に 注目 して下さい。人間 の 担当の場合 には ai_values
に "人間"
が登録 されるため、次の繰り返し処理 が 人間の手番 だった場合に、ai[i] in ai_values
が False
になるので もう一度 15 行目で ai_values
に "人間"
が登録 されてしまいます。
言葉だけの説明ではわかりづらいと思いますので、人間 VS 人間 の場合で 行われる処理 で、関連 する 変数の値 が どのように変化 するかを 下記の表 で 示します。なお、条件式 の 列 は、その行 の 条件式の値 を表します。表から、上記 のプログラムを実行すると、ai_list
に 人間の項目 が 2 つ登録 されてしまうことが わかります。
条件式 | i |
ai[i] |
value |
ai_values |
ai_list |
|
---|---|---|---|---|---|---|
2 行目 | 0 |
None |
[] |
[] |
||
4 行目 | False |
0 |
None |
[] |
[] |
|
6 行目 | False |
0 |
None |
[] |
[] |
|
8 行目 | 0 |
None |
"人間" |
[] |
[] |
|
13 行目 | 0 |
None |
"人間" |
["人間"] |
[] |
|
14 行目 | 0 |
None |
"人間" |
["人間"] |
[("人間", "人間)] |
|
2 行目 | 1 |
None |
["人間"] |
[("人間", "人間)] |
||
4 行目 | False |
1 |
None |
["人間"] |
[("人間", "人間)] |
|
6 行目 | False |
1 |
None |
["人間"] |
[("人間", "人間)] |
|
8 行目 | 1 |
None |
"人間" |
["人間"] |
[("人間", "人間)] |
|
13 行目 | 1 |
None |
"人間" |
["人間", "人間"] |
[("人間", "人間)] |
|
14 行目 | 1 |
None |
"人間" |
["人間", "人間"] |
[("人間", "人間), ("人間", "人間)]
|
プログラムを 確認できる ように、コメントを除いた プログラムを 下記 に 再掲 します。
2 for i in range(2):
4 if ai[i] not in ai_values:
6 if ai[i] is None:
7 label = "人間"
8 value = "人間"
9 else:
10 label = ai[i].__name__
11 value = ai[i]
13 ai_list.append((label, value))
14 ai_values.append(value)
15 select_values.append(value)
本当 は、2 回目 の 2、4、6 行目 では、value
に "人間"
が 代入されたまま になっていますが、8 行目 で 改めて value
に "人間"
が 代入される ことを 明確するため に 空欄 に しました。
バグの修正
この問題 は、4 行目 の 条件式 で、ai_values
に 実際 に 登録する値 を 使って、in 演算子 で 判定を行う ことで 解決 することが できます。そのため には、ai_values
に 登録 する value
を 計算 する 6 ~ 13 行目 にあった 処理 を、4 行目 の 条件式より も 前に移動 する 必要 が あります。下記 は、そのよう に play
メソッドを 修正 したプログラムです。
- 9 ~ 14 行目:16 行目 の if 文 の ブロックの中 に 記述 されていた 処理 を、if 文 の ブロックの前 に 移動 する
1 import matplotlib.pyplot as plt
2 import ipywidgets as widgets
3 import math
4
5 def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
6 # ai に代入されている内容を ai_list に追加する
7 for i in range(2):
8 # ラベルと項目の値を計算する
9 if ai[i] is None:
10 label = "人間"
11 value = "人間"
12 else:
13 label = ai[i].__name__
14 value = ai[i]
15 # value が ai_values に登録済かどうかを判定する
16 if value not in ai_values:
17 # 項目を登録する
18 ai_list.append((label, value))
19 ai_values.append(value)
20 select_values.append(value)
元と同じなので省略
21
22 Marubatsu.play = play
行番号のないプログラム
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# ai_list から、項目だけを取り出した list を作成する
ai_values = [value for label, value in ai_list]
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# ai に代入されている内容を ai_list に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value が ai_values に登録済かどうかを判定する
if value not in ai_values:
# 項目を登録する
ai_list.append((label, value))
ai_values.append(value)
select_values.append(value)
# 〇 と × の Dropdown を作成する
dropdown_circle = widgets.Dropdown(
options=ai_list,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_list,
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)
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_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# ai に代入されている内容を ai_list に追加する
for i in range(2):
# ラベルと項目の値を計算する
+ if ai[i] is None:
+ label = "人間"
+ value = "人間"
+ else:
+ label = ai[i].__name__
+ value = ai[i]
# value が ai_values に登録済かどうかを判定する
if value not in ai_values:
- if ai[i] is None:
- label = "人間"
- value = "人間"
- else:
- label = ai[i].__name__
- value = ai[i]
# 項目を登録する
ai_list.append((label, value))
ai_values.append(value)
select_values.append(value)
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のような エラーが発生 します。エラーが発生 する 原因 について少し 考えてみて下さい。
mb.play(ai=[None, None], gui=True)
実行結果
略
38 dropdown_cross = widgets.Dropdown(
39 options=ai_list,
40 description="×",
41 layout=widgets.Layout(width="100px"),
42 style={"description_width": "20px"},
---> 43 value=select_values[1],
44 )
略
IndexError: list index out of range
エラーの検証
エラーメッセージ から、---> 43 value=select_values[1],
の 行 で、list の インデックス に 範囲外(out of range)の 値が設定 されたこと が 原因 であることが わかります。
先程 と 同様 に、人間 VS 人間 の場合で行われる 処理 で、関連 する 変数の値 が どのように変化 するかを 下記 の 表 で 示します。
条件式 | i |
ai[i] |
value |
ai_values |
select_values |
|
---|---|---|---|---|---|---|
1 行目 | 0 |
None |
[] |
[] |
||
2 行目 | False |
0 |
None |
[] |
[] |
|
4 行目 | 0 |
None |
"人間" |
[] |
[] |
|
8 行目 | False |
0 |
None |
"人間" |
[] |
[] |
10 行目 | 0 |
None |
"人間" |
["人間"] |
[] |
|
11 行目 | 0 |
None |
"人間" |
["人間"] |
["人間"] |
|
1 行目 | 1 |
None |
["人間"] |
["人間"] |
||
2 行目 | False |
1 |
None |
["人間"] |
["人間"] |
|
4 行目 | 1 |
None |
"人間" |
["人間"] |
["人間"] |
|
8 行目 | True |
1 |
None |
"人間" |
["人間"] |
["人間"] |
プログラムを 確認できる ように、コメントを除いた プログラムを 下記 に 再掲 します。
1 for i in range(2):
2 if ai[i] is None:
3 label = "人間"
4 value = "人間"
5 else:
6 label = ai[i].__name__
7 value = ai[i]
8 if value not in ai_values:
9 ai_list.append((label, value))
10 ai_values.append(value)
11 select_values.append(value)
2 回目 の 繰り返し の 8 行目 の 条件式 が ai[i] not in ai_values
から、value not in ai_values
に 変わった ため True
になる ので、上記の表 のように、select_values
には、["人間"]
という、要素 が 1 つ しかない list が 代入 されます。そのため、select_values[1]
を 参照 しようとすると、IndexError: list index out of range
という エラーが発生 します。
select_values
は、〇 と × の 両方の手番 の 項目の値 を 持つ必要 が あります。上記 のプログラムの 問題点 は、11 行目 の select_values
に value
を 追加する処理 が、8 行目 の if 文 によって value
が ai_values
に 登録されていない場合 でしか 行われない点 です。従って、下記 のプログラムように、select_values
に value
を 追加 する 処理 を 8 行目 の 前に移動 することで、この 問題を解決 することが できます。
- 12 行目:14 行目 の if 文 の 中 に 記述 されていた 処理 を if 文 の 前に移動 する
1 def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # ai に代入されている内容を ai_list に追加する
3 for i in range(2):
4 # ラベルと項目の値を計算する
5 if ai[i] is None:
6 label = "人間"
7 value = "人間"
8 else:
9 label = ai[i].__name__
10 value = ai[i]
11 # value を select_values に常に登録する
12 select_values.append(value)
13 # value が ai_values に登録済かどうかを判定する
14 if value not in ai_values:
15 # 項目を登録する
16 ai_list.append((label, value))
17 ai_values.append(value)
元と同じなので省略
18
19 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# ai_list から、項目だけを取り出した list を作成する
ai_values = [value for label, value in ai_list]
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# ai に代入されている内容を ai_list に追加する
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_values:
# 項目を登録する
ai_list.append((label, value))
ai_values.append(value)
# 〇 と × の Dropdown を作成する
dropdown_circle = widgets.Dropdown(
options=ai_list,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_list,
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)
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# ai に代入されている内容を ai_list に追加する
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_values:
# 項目を登録する
ai_list.append((label, value))
ai_values.append(value)
- select_values.append(value)
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のように 正しい処理 が 行われる ことが 確認 できます。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
バグその 2(前に表示された項目の内容が残る)
play
メソッドには もう一つバグ がありますが、そのバグは 初心者 には 気づきづらい バグだと思います。具体的 には、上記で 人間 VS 人間 の 対戦 を 行った後 で、下記 のプログラムで AI VS AI の 対戦 を行うと、実行結果 の 左下図 のように、一見 すると それぞれ の Dropdown に 正しい AI が 選択 されているので、うまくいっている ように 見えるかもしれません が、Dropdown の上 で マウスを押す と、右下図 のように、今回 は 対戦 を 行っていない、人間の項目 が 表示 されるという 問題が発生 します。
from ai import ai1s, ai2s
mb.play(ai=[ai1s, ai2s], gui=True);
実行結果(下図は、画像なので操作することはできません)
バグの原因の検証
この バグの原因 を Python の 初心者が気づく のは 難しい かもしれません。実は 筆者 も 上記 のプログラムが 正しい と最初は 勘違い していました。この バグの原因 は、下記 のプログラムの 2 行目 のように、play
メソッドの 最初 に、仮引数 ai_list
の 値を表示 することで 確認 することができます。
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
print("ai_list =", ai_list)
元と同じなので省略
Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
print("ai_list =", ai_list)
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# ai_list から、項目だけを取り出した list を作成する
ai_values = [value for label, value in ai_list]
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# ai に代入されている内容を ai_list に追加する
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_values:
# 項目を登録する
ai_list.append((label, value))
ai_values.append(value)
# 〇 と × の Dropdown を作成する
dropdown_circle = widgets.Dropdown(
options=ai_list,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_list,
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)
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
+ print("ai_list =", ai_list)
元と同じなので省略
Marubatsu.play = play
上記の修正 を 行った後 で、先程 と 同じ順番 で、人間 VS 人間 と AI VS AI の 対戦 を行います。下記 は 人間 VS 人間 の 対戦 を 行った場合 です。なお、実行結果 にはこの バグの検証 に 必要 な print(ai_list)
の 表示のみ を 表記 します。
mb.play(ai=[None, None], gui=True);
実行結果
ai_list = []
上記 のプログラムは、仮引数 ai_list
に 対応 する 実引数 を 記述していない ので、ai_list
には デフォルト値 である []
が 代入 され、実行結果 に 表示 されます。
続けて 下記のプログラムで、AI VS AI の 対戦 を行います。
mb.play(ai=[ai1s, ai2s], gui=True);
実行結果
ai_list = [('人間', '人間')]
実行結果 から、先程と同様 に 仮引数 ai_list
に 対応する実引数 を 記述していない にも かかわらず、ai_list
に [('人間', '人間')]
が 代入されている ことが 確認 できます。これが、Dropdown に 人間の項目 が 表示 されてしまう 原因 です。
この現象 の 原因 については、本記事で デフォルト引数 の 説明 を 最初に行った 以前の記事で 説明 しましたが、〇×ゲーム を 実装する際 に そのような例 がこれまで 出てこなかったの で、ほとんどの方は忘れてしまったのではないかと思いますので、もう一度説明します。
デフォルト引数 の デフォルト値 を表す オブジェクト は、関数の定義 が 実行 された 際 に 1 回だけ作成 され、デフォルト引数 に 対応 する 実引数 が 記述されなかった 場合は、そのオブジェクト が デフォルト引数 に 代入 されます。関数呼び出し が 行われた際 に、毎回 デフォルト値を表す オブジェクト が 新しく作成 される わけではない 点に 注意 して下さい。
従って、デフォルト値 に list など の ミュータブル な オブジェクト を 設定 し、関数 の ブロックの中 で、デフォルト値 の 要素 の 値を変更 したり、要素を追加 するなどの 処理 を行うと、デフォルト値 の 内容 が 変化 します。
実際 に play
メソッドでは、ai_list.append((label, value))
という、ai_list
に デフォルト値 が 代入 されていた場合に、その 要素を追加 するという 処理 を 行っています。そのため、ai_list
に 対応 する 実引数 を 記述せず に 人間 VS 人間 の 対戦を行う と、デフォルト値 が 空の list から、[("人間", "人間")]
に 変化 してしまいます。そして、その値 が、次 に AI VS AI の 対戦 を play
メソッドで 行った際 に 下記 のように 表示 されます。
ai_list = [('人間', '人間')]
この問題 が 発生しない ようにするためには、下記 のプログラムのように、list など の ミュータブルな値 を デフォルト値 に したい場合 は、デフォルト値 に None
を設定 し、関数 の ブロックの中 で、デフォルト引数の値 が None
の場合に、デフォルト引数 に 実際 に 設定したい値 を 代入 するという 処理 を 記述する必要 が あります。このように記述 することで、デフォルト引数 に 対応 する 実引数 を 記述しなかった 場合に、デフォルト引数 に 新しい値 が 毎回代入 されるので 上記 のような 問題 は 発生しなくなります。
-
1 行目:
ai_list
の デフォルト値 をNone
に 修正 する -
3、4 行目:
ai_list
がNone
の場合 に、ai_list
に 空の list を 代入 する
1 def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
2 # ai_list が None の場合は、空の list で置き換える
3 if ai_list is None:
4 ai_list = []
元と同じなので省略
5
6 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# ai_list が None の場合は、空の list で置き換える
if ai_list is None:
ai_list = []
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# ai_list から、項目だけを取り出した list を作成する
ai_values = [value for label, value in ai_list]
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# ai に代入されている内容を ai_list に追加する
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_values:
# 項目を登録する
ai_list.append((label, value))
ai_values.append(value)
# 〇 と × の Dropdown を作成する
dropdown_circle = widgets.Dropdown(
options=ai_list,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_list,
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)
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
-def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
+def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# ai_list が None の場合は、空の list で置き換える
+ if ai_list is None:
+ ai_list = []
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のように 正しい処理 が 行われる ことが 確認 できます。人間 VS 人間 の 実行結果 は 省略 します。
なお、下記 のプログラムを 同じセルで実行 すると、2 つ の対戦の Dropdown の表示 が ゲーム盤よりも前 に 並べて表示 されるので、別々のセル で 実行 して下さい。
mb.play(ai=[None, None], gui=True);
mb.play(ai=[ai1s, ai2s], gui=True);
実行結果(下図は、画像なので操作することはできません)
デフォルト引数 params
の修正
上記の問題 は、下記 のような 処理 を 行う際 に 発生 します。
- デフォルト値 に list など の ミュータブル な データ を 代入 する
- 関数 の ブロック内 で、デフォルト引数 の 要素 を 変更する処理 を 行う
逆に言えば、上記 のような 処理 を 行わない 場合は、上記の問題 は 発生しません。例えば、play
メソッドには、params=[{}, {}]
のように、デフォルト値 に list のような ミュータブル な 値 が 設定 された デフォルト引数 が 存在 しますが、params
は、関数 の ブロックの中 で、その 要素 を 変更 することは ない ので、このまま でも 問題 は 発生しません。
ただし、今後 params
の 要素 を 変更 するような 処理 を 行う可能性 が ない とは 言い切れません し、要素 を 変更することがない と 勘違い している場合が あるかもしれない ので、ミュータブル な データ を デフォルト値 に 設定したい 場合は、特別な理由 が ない限り、常にデフォルト値 に None
を 設定 し、None
の場合 に 設定したい値 を 代入する という 処理 を 記述したほうが良い でしょう。
下記 は、そのよう に play
メソッドを 修正 した プログラム です。
-
1 行目:
params
の デフォルト値 をNone
に 修正 する -
6、7 行目:
params
がNone
の場合に 元のデフォルト値 を 代入 する
1 def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):
2 # ai_list が None の場合は、空の list で置き換える
3 if ai_list is None:
4 ai_list = []
5 # params が None の場合のデフォルト値を設定する
6 if params is None:
7 params = [{}, {}]
元と同じなので省略
8
9 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_list が None の場合は、空の list で置き換える
if ai_list is None:
ai_list = []
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# ai_list から、項目だけを取り出した list を作成する
ai_values = [value for label, value in ai_list]
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# ai に代入されている内容を ai_list に追加する
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_values:
# 項目を登録する
ai_list.append((label, value))
ai_values.append(value)
# 〇 と × の Dropdown を作成する
dropdown_circle = widgets.Dropdown(
options=ai_list,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
options=ai_list,
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)
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_list が None の場合は、空の list で置き換える
if ai_list is None:
ai_list = []
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
元と同じなので省略
Marubatsu.play = play
実行結果 は 省略 しますが、上記 の 修正後 に、下記 のプログラムを 実行 することで、正しい処理 が 行われる ことが 確認 できます。
mb.play(ai=[None, None], gui=True)
上記の記事を記述している際に気づいたのですが、これまでは play_loop
の いくつか の 仮引数 に デフォルト値 を 設定 していました。play_loop
は、play
メソッドの中から、必ず すべての仮引数 に 対応 する 実引数を記述 して 呼び出している ので、デフォルト値 を 設定 する 意味 が ありません。そこで、下記 プログラムのように、play_loop
の デフォルト引数 を、通常 の 仮引数 に 修正 することにします。
def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
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)
# 手番を人間が担当する場合は、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)
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
-def play_loop(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
+def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
Marubatsu.play_loop = play_loop
実行結果 は 省略 しますが、上記 の 修正後 に、下記 のプログラムを 実行 することで、正しい処理 が 行われる ことが 確認 できます。
mb.play(ai=[None, None], gui=True)
細かい話 になりますが、上記 の「関数 の ブロック内 で、デフォルト引数 の 内容 を 変更する処理 を 行う」は、その関数の中 で デフォルト値 の 内容 を 変更しなくても、デフォルト値 を 他の関数呼び出し の 実引数に記述 し、呼び出された関数の中 で デフォルト値の内容 を 変更する 場合も 含みます。
例えば、下記 のプログラムは、a
から 呼び出した 関数 b
の中 で デフォルト値 に 要素を追加 するので、a
を 呼び出すたび に、x
の デフォルト値 は 変化 します。
def a(x=[]):
print(x)
b(x)
def b(x):
x.append(1)
a()
a()
実行結果
[]
[1]
GUI でゲームを遊ぶ関数の定義
前回の記事で、循環インポート の エラーを解決 するために、ai1s
~ ai14s
を Dropdown に 登録する処理 を play
メソッドから 削除 したため、それらの AI を Dropdown で 選択できる ようにするためには、下記 のプログラムのように、ai1s
~ ai14s
の データが登録 された ai_list
を 作成する処理 を 記述する必要 が あります。
import ai
# Dropdown の作成の際に必要となる、AI のリストを作成する
ai_list = []
for i in range(1, 15):
ai_name = f"ai{i}s"
ai_list.append((ai_name, getattr(ai, ai_name)))
mb.play(ai=[None, None], ai_list=ai_list, gui=True)
実行結果(下図は、画像なので操作することはできません)
毎回 このような 処理を記述 するのは 大変 なので、GUI で ai1s
~ ai14s
の 14 種類 の AI を 選択 する Dropdown を 表示 して 〇×ゲームを遊ぶ ための 下記 のような 関数を定義 することにします。
名前:GUI で 遊ぶ(play)ので、gui_play
とする
処理:GUI で ai1s
~ ai14s
の AI を 選択 する Dropdown を 表示 して 〇×ゲームを遊ぶ
入力:最初の担当 と、Dropdown に 表示する項目 を 自由 に 設定できる ように、play
メソッドと 同じ意味 を持つ 仮引数 ai
と ai_list
を 持つ ようにする
出力:なし
gui_play
の定義
下記 は、gui_play
の 定義 です。なお、gui_play
では、ai
に 対応 する 実引数 を 記述しなかった場合 に 人間どうし の 対戦 が行われるように 工夫 してみました。
-
1 行目:
gui_play
にはai
という 仮引数 が 存在する ので、ai モジュール を再びai_module
という 別の名前 で インポート する 必要がある -
3 行目:デフォルト値 に
None
を 設定 した デフォルト引数ai
とai_list
を持つgui_play
を定義する -
5、6 行目:
ai
にNone
が代入 されていた場合は、ai
に[None, None]
を 代入 することで、人間どうし の 対戦 が行われるようにする -
8 ~ 12 行目:
ai_list
にNone
が代入 されていた場合は、ai_list
にai1s
~ai14s
の 項目 を持つ Dropdown を 作成するため の データ を 計算 して 代入 する -
14、15 行目:
Marubatsu
クラスの インスタンスを作成 し、play
メソッドで GUI で 上記の設定 で 〇×ゲームの対戦 を 行う
1 import ai as ai_module
2
3 def gui_play(ai=None, ai_list=None):
4 # ai が None の場合は、人間どうしの対戦を行う
5 if ai is None:
6 ai = [None, None]
7 # ai_list が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
8 if ai_list is None:
9 ai_list = []
10 for i in range(1, 15):
11 ai_name = f"ai{i}s"
12 ai_list.append((ai_name, getattr(ai_module, ai_name)))
13
14 mb = Marubatsu()
15 mb.play(ai=ai, ai_list=ai_list, gui=True)
行番号のないプログラム
import ai as ai_module
def gui_play(ai=None, ai_list=None):
# ai が None の場合は、人間どうしの対戦を行う
if ai is None:
ai = [None, None]
# ai_list が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
if ai_list is None:
ai_list = []
for i in range(1, 15):
ai_name = f"ai{i}s"
ai_list.append((ai_name, getattr(ai_module, ai_name)))
mb = Marubatsu()
mb.play(ai=ai, ai_list=ai_list, gui=True)
gui_play
の動作の確認
下記 のプログラムで、gui_play
に 実引数 を 記述せず に呼び出した場合は、GUI で 人間 VS 人間 の 対戦 が 行われる ことが 確認 できます。
gui_play()
実行結果(下図は、画像なので操作することはできません)
また、下記 のプログラムで、人間 VS ai1s
の 対戦 が 行われる ことが 確認 できます。
gui_play(ai=[None, ai1s])
実行結果(下図は、画像なので操作することはできません)
また、下記 のプログラムで、ai1
のみ が 登録 された ai_list
のデータを 作成 して キーワード引数 に 記述 した場合でも gui_play
が 正しく動作 することが 確認 できます。
gui_play(ai_list=[("ai1s", ai.ai1s)])
実行結果(下図は、画像なので操作することはできません)
実は、gui_play
には 問題 が あります。それが何か少し考えてみて下さい。
人間の項目の登録
gui_play
には、下記 のプログラムのように、AI どうし の 対戦 を行う場合、実行結果 の 右下図 のように、Dropdown に 人間の項目 が 表示されない という 問題 が あります。
gui_play(ai=[ai1s, ai1s])
実行結果(下図は、画像なので操作することはできません)
先程、gui_play()
を 実行 した際に、下図 のように 人間の項目 が 最後に表示 されたのは、人間 VS 人間 の 対戦 が行われたため、play
メソッドの 中で、ai_list
に 登録されていない 人間の項目が 自動的 に 登録された からです。
この問題 は、下記 のプログラムの 7 行目 のように、gui_play
の中の、ai_list
の 初期化処理 で、人間の項目 の データ を 代入 することで 解決 できます。
1 def gui_play(ai=None, ai_list=None):
2 # ai が None の場合は、人間どうしの対戦を行う
3 if ai is None:
4 ai = [None, None]
5 # ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
6 if ai_list is None:
7 ai_list = [("人間", "人間")]
8 for i in range(1, 15):
9 ai_name = f"ai{i}s"
10 ai_list.append((ai_name, getattr(ai_module, ai_name)))
11
12 mb = Marubatsu()
13 mb.play(ai=ai, ai_list=ai_list, gui=True)
行番号のないプログラム
def gui_play(ai=None, ai_list=None):
# ai が None の場合は、人間どうしの対戦を行う
if ai is None:
ai = [None, None]
# ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
if ai_list is None:
ai_list = [("人間", "人間")]
for i in range(1, 15):
ai_name = f"ai{i}s"
ai_list.append((ai_name, getattr(ai_module, ai_name)))
mb = Marubatsu()
mb.play(ai=ai, ai_list=ai_list, gui=True)
修正箇所
def gui_play(ai=None, ai_list=None):
# ai が None の場合は、人間どうしの対戦を行う
if ai is None:
ai = [None, None]
# ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
if ai_list is None:
- ai_list = []
+ ai_list = [("人間", "人間")]
for i in range(1, 15):
ai_name = f"ai{i}s"
ai_list.append((ai_name, getattr(ai_module, ai_name)))
mb = Marubatsu()
mb.play(ai=ai, ai_list=ai_list, gui=True)
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のように 正しい処理 が 行われる ことが 確認 できます。なお、人間の項目 を gui_play
の中 で 最初に登録 したので、先程 と 異なり、人間の項目 が 最初に表示 されるようになります。
gui_play(ai=[ai1s, ai1s])
実行結果(下図は、画像なので操作することはできません)
ai1s
~ ai14s
以外の AI で対戦を行う場合の確認
下記 のプログラムで、gui_play
を使って、ai1s
~ ai14s
以外 の AI で 対戦を行う 場合を 確認 します。実行結果 から、正しい処理 が行われることが 確認 できます。
from ai import ai1, ai2
gui_play(ai=[ai1, ai2])
実行結果(下図は、画像なので操作することはできません)
gui_play
の記述場所
gui_play
を Marubatsu クラスの メソッド として 定義しなかった 点が 気になってい る人が いるかもしれません が、gui_play
の中 で ai モジュール の 関数 を 利用している ので、gui_play
を Marubatsu クラスの メソッド として 定義 すると、前回の記事で説明したように、インポートの循環 の エラーが発生 してしまいます。
また、gui_play
は、AI の 処理を行う 関数 ではない ので、ai モジュール の 中に記述 するのは 少し不自然 です。そこで、本記事では gui_play
を util.py という 名前 の ファイルに保存する ことにします。util.py の中 で marubatsu と ai の 両方のモジュール を インポート しても、インポートの循環 は 起こらない ので エラー は 発生しません。
なお、util とは、小規模 で、補助的 な 機能 を持つ プログラム を表す ユーティリティ(utility)の 略 で、比較的 良く使われるプログラミング用語 です。
dict を用いた Dropdown のラベルの設定
以前の記事で、Dropdown の ラベル を 設定する方法 として、options
属性 の 値 に (ラベル, 項目の値)
という tuple を 要素 として持つ list で 設定 する方法を 紹介 しましたが、それ以外 にも { "ラベル": 項目の値 }
という dict で設定 することが できます。また、dict
で設定 することで、play
メソッドの 処理 を より簡潔に記述 することが できるようになる ことに気が付きましたので、その方法を紹介することにします。
前回の記事では、ai
の内容 を Dropdown に 自動登録 する 処理 を実装しましたが、その際に、ai_list
の中 に、ai
の 要素 が 含まれているか を 判定する必要 が あります。
ai_list
の 要素 には、(ラベル, 項目の値)
という tuple が 代入 されているため、in 演算子 を使って その判定 を行うことは できません。そのため、ai_list
の中 から 項目の値 を 取り出した list を 作成 して ai_values
に 代入 するという 処理 を、下記 のプログラムのように リスト内包表記 を用いて行いました。
# ai_list から、項目だけを取り出した list を作成する
ai_values = [value for label, value in ai_list]
上記の処理 は、ai_list
を、それぞれの項目 を { "ラベル": 項目の値 }
という キー と キーの値 を持つ dict で設定 することで、簡潔に記述 することが できます。
具体的には、下記 のプログラムのように、dict の キーの値 の 一覧 を表す list を 計算 する values
という メソッド を 利用 します。
ai_values = ai_list.values()
ai_list
を dict で表現 することで、play
メソッドを下記のように修正します。なお、ai_list
という 名前のまま では 変 なので、ai_dict
という 名前に変更 しました。
-
1 行目:仮引数
ai_list
をai_dict
に 変更 する -
4 行目:
ai_dict
がNone
の場合に 代入する値 を 空の dict に 修正 する -
14 行目:この下にあった
ai_values
を 計算 する 処理 を 削除 する -
18 行目:
ai_values
の代わりに、ai_dict.values()
で 判定を行う ように 修正 する -
20 行目:
ai_dict
のlabel
の キーの値 にvalue
を 代入 する -
21 行目:この下にあった
ai_values
に 項目を登録 する 処理 を 削除 する -
3、25、32 行目:
ai_list
をai_dict
に 変更 する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
2 # ai_dict が None の場合は、空の list で置き換える
3 if ai_dict is None:
4 ai_dict = {}
元と同じなので省略
5 # ai に代入されている内容を ai_dict に追加する
6 for i in range(2):
7 # ラベルと項目の値を計算する
8 if ai[i] is None:
9 label = "人間"
10 value = "人間"
11 else:
12 label = ai[i].__name__
13 value = ai[i]
14 # この下にあった、ai_values を計算する処理を削除する
15 # value を select_values に常に登録する
16 select_values.append(value)
17 # value が ai_values に登録済かどうかを判定する
18 if value not in ai_dict.values():
19 # 項目を登録する
20 ai_dict[label] = value
21 # この下にあった、ai_values に value を登録する処理を削除する
22
23 # 〇 と × の Dropdown を作成する
24 dropdown_circle = widgets.Dropdown(
25 options=ai_dict,
26 description="〇",
27 layout=widgets.Layout(width="100px"),
28 style={"description_width": "20px"},
29 value=select_values[0],
30 )
31 dropdown_cross = widgets.Dropdown(
32 options=ai_dict,
33 description="×",
34 layout=widgets.Layout(width="100px"),
35 style={"description_width": "20px"},
36 value=select_values[1],
37 )
元と同じなので省略
38
39 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)
# それぞれの手番の担当を表す 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)
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
-def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):
+def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
- if ai_list is None:
+ if ai_dict is None:
- ai_list = []
+ ai_dict = {}
元と同じなので省略
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
- ai_values = [value for label, value in ai_list]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
- if value not in ai_values:
+ if value not in ai_dict.values():
# 項目を登録する
- ai_list.append((label, value))
+ ai_dict[label] = value
- ai_values.append(value)
# 〇 と × の Dropdown を作成する
dropdown_circle = widgets.Dropdown(
- options=ai_list,
+ options=ai_dict,
description="〇",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
dropdown_cross = widgets.Dropdown(
- options=ai_list,
+ options=ai_dict,
description="×",
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[1],
)
元と同じなので省略
Marubatsu.play = play
次に、gui_play
の 中 の ai_dict
を 作成する処理 を、下記 のように 修正 します。
-
1 行目:仮引数
ai_list
をai_dict
に 変更 する -
7 行目:
ai_dict
に、人間の項目 を表す dict を 代入 するように 修正 する -
10 行目:
ai_dict
のai_name
の キーの値 に、getattr
で 計算 した AI の関数 を 代入 する -
6、13 行目:
ai_list
をai_dict
に 変更 する
1 def gui_play(ai=None, ai_dict=None):
2 # ai が None の場合は、人間どうしの対戦を行う
3 if ai is None:
4 ai = [None, None]
5 # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
6 if ai_dict is None:
7 ai_dict = { "人間": "人間"}
8 for i in range(1, 15):
9 ai_name = f"ai{i}s"
10 ai_dict[ai_name] = getattr(ai_module, ai_name)
11
12 mb = Marubatsu()
13 mb.play(ai=ai, ai_dict=ai_dict, gui=True)
行番号のないプログラム
def gui_play(ai=None, ai_dict=None):
# ai が None の場合は、人間どうしの対戦を行う
if ai is None:
ai = [None, None]
# ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
if ai_dict is None:
ai_dict = { "人間": "人間"}
for i in range(1, 15):
ai_name = f"ai{i}s"
ai_dict[ai_name] = getattr(ai_module, ai_name)
mb = Marubatsu()
mb.play(ai=ai, ai_dict=ai_dict, gui=True)
修正箇所
-def gui_play(ai=None, ai_list=None):
+def gui_play(ai=None, ai_dict=None):
# ai が None の場合は、人間どうしの対戦を行う
if ai is None:
ai = [None, None]
# ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
- if ai_list is None:
+ if ai_dict is None:
- ai_list = [("人間", "人間")]
+ ai_dict = { "人間": "人間"}
for i in range(1, 15):
ai_name = f"ai{i}s"
- ai_list.append((ai_name, getattr(ai_module, ai_name)))
+ ai_dict[ai_name] = getattr(ai_module, ai_name)
mb = Marubatsu()
- mb.play(ai=ai, ai_list=ai_list, gui=True)
+ mb.play(ai=ai, ai_dict=ai_dict, gui=True)
上記 の 修正後 に、下記 のプログラムで、gui_play
に 実引数 を 記述せず に呼び出した場合は、GUI で 人間 VS 人間 の 対戦 が 正しく行われる ことが 確認 できます。
gui_play()
実行結果(下図は、画像なので操作することはできません)
本記事では省略しますが、余裕がある方 は先程行った gui_play(ai=[ai1s, ai1s])
など を 実行 して gui_play
が 正しく動作する ことを 確認 してみて下さい。
今回の記事のまとめ
今回の記事では、play
メソッドの バグの修正 を行いました。また、その際に、デフォルト引数 の デフォルト値 に list など の ミュータブル な データ を 代入 した場合の 注意点 と 対処法 について 説明 しました。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
以下のリンクは、今回の記事で作成した util.py です。
次回の記事