目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
クラスによる GUI の機能の分離(続き)
前回の記事では、Marubatsu_GUI
クラスを定義 することで、Marubatsu
クラスから GUI の機能を分離 する作業を開始しました。
前回の記事では比較的スムーズに分離の作業が行えましたが、今回の記事では、作業を行うたびに様々な問題が発生し、それを修正するという、かなり面倒な作業を何度も行います。
もちろん、プログラミングの技能が上達すれば、発生する可能性のある問題をあらかじめ考慮しながら作業を行うことで、今回の記事よりも効率よく作業を行うことができるようになりますが、そのためには実際に面倒な作業を体験することが重要だと思います。また、今回の記事のような面倒な作業は、プログラムの構造を変更するような修正を行う際に、実際に行う必要がある場合がよくあるので紹介することにします。
ゲーム盤の画像を描画するの関数の定義
前回の記事の最後で、ゲーム盤を描画する Figure を作成したので、次は ゲーム盤の画像を描画 する処理を行っていた Marubatsu
クラスの draw_board
を Marubatsu_GUI
クラスのメソッドとして定義し直す ことにします。
set_button_status
と update_widgets_status
メソッドの定義
前回の記事で、「必要がない限り、ローカル変数やローカル関数を使わずに、属性とメソッドを利用する」という方針をとることにしたので、draw_board
のローカル関数 として定義されていた set_button_status
と update_widgets_status
を、下記のプログラムのように Marubatsu_GUI
のメソッドとして定義 することにします。
-
3 ~ 7 行目:
set_button_status
は、Marubatsu_GUI
クラスの情報を利用しない ので、@staticmethod
を使って 静的メソッドとして定義 する。なお、この部分を通常のメソッドとして定義しても問題はない -
12 ~ 17 行目:
update_widgets_status
をMarubatsu_GUI
のメソッドとして定義する
1 from marubatsu import Marubatsu_GUI
2
3 @staticmethod
4 # ボタンのウィジェットの状態を設定する
5 def set_button_status(button, disabled):
6 button.disabled = disabled
7 button.style.button_color = "lightgray" if disabled else "lightgreen"
8
9 Marubatsu_GUI.set_button_status = set_button_status
10
11 # ウィジェットの状態を更新する
12 def update_widgets_status(self):
13 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
14 set_button_status(self.first_button, self.move_count <= 0)
15 set_button_status(self.prev_button, self.move_count <= 0)
16 set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
17 set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
18
19 Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
from marubatsu import Marubatsu_GUI
@staticmethod
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
button.disabled = disabled
button.style.button_color = "lightgray" if disabled else "lightgreen"
Marubatsu_GUI.set_button_status = set_button_status
# ウィジェットの状態を更新する
def update_widgets_status(self):
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
set_button_status(self.first_button, self.move_count <= 0)
set_button_status(self.prev_button, self.move_count <= 0)
set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
from marubatsu import Marubatsu_GUI
+@staticmethod
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
button.disabled = disabled
button.style.button_color = "lightgray" if disabled else "lightgreen"
Marubatsu_GUI.set_button_status = set_button_status
# ウィジェットの状態を更新する
-def update_widgets_status():
+def update_widgets_status(self):
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
set_button_status(self.first_button, self.move_count <= 0)
set_button_status(self.prev_button, self.move_count <= 0)
set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
Marubatsu_GUI.update_widgets_status = update_widgets_status
draw_board
の定義
次に、draw_board
を marubatsu_GUI
のメソッドとして定義 する必要がありますが、その際に 下記の点に注意 する必要があります。
-
Marubatsu
クラスのdraw_board
のself
は、Marubatsu
クラスのインスタンス である -
Marubatsu_GUI
クラスのdraw_board
のself
は、Marubatsu_GUI
クラスのインスタンス である -
Marubatsu_GUI
クラスのインスタンスのmb
という属性 に、Marubatsu
クラスのインスタンスが代入 されている - 従って、
Marubatsu
クラスのdraw_board
をMarubatsu_GUI
クラスのdraw_board
として定義し直す 場合は、self
をself.mb
に修正する 必要がある
置換の機能の問題点
これまでの記事では、名前を変更する際 に、変更箇所が多くなかったので、変更する場所を自分で見つけて修正 を行っていましたが、上記の修正は 変更箇所が多い ので、変更箇所を 自分で探して変更するのは大変 なだけでなく、間違えやすい という問題があります。
一般的な文章を編集するソフトでは、draw_board
の self
を self.mb
に修正するような、置換の作業を大量に行う際 に、置換の機能を使うのが一般的 です。VSCode にも置換を行う機能がありますが、下記のような問題があるので、この後で説明する シンボル名の変更 の機能を使ったほうが良いでしょう。
- 置換の機能は、プログラムの文脈を考慮せず に、指定した文字列を置換の対象とする
- そのため、置換の機能を利用する際は、置換を行う文字列を 一つ一つ検索 して探し、その文字列を 置換しても良いかどうかを確認しながら行う必要 がある
具体例を示します。下記のプログラムで、グローバル変数 i
の名前を j
に変更する必要が生じたと思ってください。
i = 1 # この i はグローバル変数
def f():
i = 1 # この i はローカル変数
print(i) # この i はローカル変数
print(i) # この i はグローバル変数
VSCode の置換の機能を使ってこの変更を行う場合は、下記のような手順で行います。
-
下図のように、上のテキストボックスに
i
を、下のテキストボックスにj
を入力する。なお、「1/26件」はファイルの中に 26 か所のi
が検索され、その中の 1 つ目のi
が選択状態になっていることを表す -
下図左の赤丸の「すべて置換」ボタンをクリックすると、このセルだけでなく、VSCode で編集中のファイルの すべての
i
がj
に置換されてしまう のでこの場合は 行ってはならない。例えば、上図のプログラムの場合は、下図右のようにローカル変数i
、print
の中のi
、コメントの中のi
までもがj
に変換されてしまうため、うまくいかない
-
下図左の赤丸の矢印のボタンをクリックすることで、
i
が順番に検索されて選択状態になるので、置換する必要があるi
を検索状態にする。下図右は、プログラムが記述されたセルの、最初のi
を選択状態(灰色で表示される)にした図である
-
上記の作業を、置換する必要があるすべての
i
に対して行う
置換の作業は、上記の例のように、置換する必要がある i
と 置換してはいけない i
が 混在する ような場合は、かなり面倒 で、間違えやすい 作業が必要になります。
VSCode のシンボル名の変更の使い方
VSCode には、プログラムの文法から、関連する名前だけを置換 するという、「シンボル名の変更」1という機能があります。上記のプログラムのグローバール変数 i
を j
に変更する作業は、シンボル名の変更を使って下記の手順で 簡単に行うことができます。
-
変更するグローバル変数
i
をマウスでドラッグして 選択状態にする
シンボル名の変更の注意点
シンボル名の変更は、プログラムの文法上 での 同じ意味の名前だけが置換 される非常に便利な機能ですが、下記の点に注意して下さい
-
コメントの内容 は、プログラムで処理を行う内容ではない ので、変更されない。実際に、上図のように、コメントの
i
は変更されない - 具体例はこの後で紹介するが、変更してはいけない名前が変更される場合がある
- シンボル名の変更は、文法的に同じ意味を持つ名前を変更するので、下記のプログラムの
a
のように、文法的に正しくない名前(a
はグローバル変数として存在せず、ローカル変数としても定義されていない)を 変換することはできない
b = 0
def f():
print(a)
print(a * 2)
上記の a
の、グローバル変数 b
への変更は、下記の手順で行うことができます。
- 下記のプログラムの 4 行目のように、
a = 0
を記述してa
を 文法的に正しいローカル変数にする
1 b = 0
2
3 def f():
4 a = 0
5 print(a)
6 print(a * 2)
行番号のないプログラム
b = 0
def f():
a = 0
print(a)
print(a * 2)
- シンボル名の変更で
a
をb
に変更し、下図のようなプログラムにする
1 b = 0
2
3 def f():
4 b = 0
5 print(b)
6 print(b * 2)
- 文法的に正しくするために追加した 4 行目のプログラムを削除する
draw_board
の定義
Marubatsu_GUI
のメソッドとして draw_board
を定義する際に、下記の作業を行えば良いと思った人がいるかもしれません。
-
Marubatsu
クラスのdraw_board
の定義をそのままコピーする - ローカル関数
set_button_status
とupdate_widgets_status
を削除する - シンボル名の変更を使って
self
をself.mb
に変更する
しかし、上記の作業を行うと、下記のプログラムのような修正が行われ、実行すると実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。なお、シンボル名の変更による修正箇所は自動的に行われるので、修正箇所は省略します。
def draw_board(self.mb, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.mb.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.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.mb.BOARD_SIZE):
ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.mb.BOARD_SIZE):
for x in range(self.mb.BOARD_SIZE):
color = "red" if (x, y) == self.mb.last_move else "black"
self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)
update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
実行結果
Cell In[4], line 1
def draw_board(self.mb, ax, ai):
^
SyntaxError: invalid syntax
エラーの検証と修正
エラーメッセージから、1 行目の draw_board
の定義の 仮引数の部分に self.mb
が記述される という、文法エラー(syntax error)が発生 していることがわかります。これは、シンボル名の変更で self
を self.mb
に変更した際に、変更してはいけない draw_board
の仮引数 self
までもが変更 されてしまったことが原因です。
上記で行ったシンボル名の変更の機能に不具合があったのではないかと思う人がいるかもしれませんが、そうではありません。シンボル名の変更 が行う処理は、文法的に同じ意味を持つ名前を変更 するという処理です。draw_board
の 仮引数 の self
と ブロックの中の self
は、文法的には同じ意味を持つ ので、シンボル名の変更 によって両方が self.mb
に変更されるのは、正しい処理 です。
上記のような間違った変換が行われた原因は、draw_board
の self
の意味 が Marubatsu
クラスと Marubatsu_GUI
クラスで下記の表のように 異なるから です。
Marubatsu クラス |
Marubatsu_GUI クラス |
|
---|---|---|
仮引数の self |
Marubatsu のインスタンス |
Marubatsu_GUI のインスタンス |
ブロックの中の self |
Marubatsu のインスタンス |
Marubatsu のインスタンス |
上記の表のように、Marubatsu
クラスの draw_board
メソッド内の self
は すべて Marubatsu
クラスのインスタンス ですが、Marubatsu_GUI
クラスの draw_board
メソッド内の self
は、仮引数とブロックの中で異なる意味 を持ちます。そのような違いがあるにも関わらず、Marubatsu
クラスの draw_board
をそのままコピーして Marubatsu_GUI
クラスの draw_board
として定義したため、文法上 の仮引数 self
の意味と、実際 の仮引数self
の意味が 食い違う という状況が発生することになります。これがエラーの原因です。
このように、あるクラスのメソッドを、別のクラスのメソッドとして定義し直す際に、異なる意味を持つ self
が混在 する場合があります。そのような場合は、シンボル名の変更 によって self
を変更した際に、変更してはいけない self
がまでもが 変更されてしまう場合がある という点に注意して下さい。
Python では 慣習的に self
を、クラスのインスタンスを代入する仮引数の名前として利用します が、そのせいで、self
という名前の意味を混同することによるエラーが発生 する場合が良くあります。実際に、この後で発生するエラーの多くは self
に関するものです。
なお、このような、異なるものに対して同じ名前が付けられてしまう という現象は、self
以外でも、プログラムの修正 による バグの原因 として 頻繁に発生する ので注意が必要です。
この問題は、下記のプログラムのように、draw_board
の仮引数を self
にすることで修正することができます。
def draw_board(self, ax, ai):
元と同じなので省略
Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.mb.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.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.mb.BOARD_SIZE):
ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.mb.BOARD_SIZE):
for x in range(self.mb.BOARD_SIZE):
color = "red" if (x, y) == self.mb.last_move else "black"
self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)
update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
修正箇所
-def draw_board(self.mb, ax, ai):
+def draw_board(self, ax, ai):
元と同じなので省略
Marubatsu_GUI.draw_board = draw_board
draw_board
メソッドの処理の確認
上記で定義した draw_board
メソッドが正しく動作するかどうかを、下記のプログラムで確認することにします。実行結果のように、ゲーム盤が描画されるようになりますが、同時にエラーが発生することがわかります。このエラーの原因について少し考えてみて下さい。
-
4 行目:
Marubatsu_GUI
のインスタンスを作成し、mb_gui
に代入する -
5 行目:
Marubatsu_GUI
のdraw_board
メソッドを呼び出して、ゲーム盤を描画する。draw_board
の仮引数ax
とai
に代入する値は、mb_gui
の属性に代入されるので、それを実引数に記述する
from marubatsu import Marubatsu
mb = Marubatsu()
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
実行結果(上部のボタンなどのウィジェットの画像は省略します)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 5
3 mb = Marubatsu()
4 mb_gui = Marubatsu_GUI(mb, ai=[None, None])
----> 5 mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
Cell In[5], line 44
41 color = "red" if (x, y) == self.mb.last_move else "black"
42 self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)
---> 44 update_widgets_status()
TypeError: update_widgets_status() missing 1 required positional argument: 'self'
エラーの検証と修正
エラーメッセージから、draw_board
内 で update_widgets_status
を呼び出す際 に、位置引数(positional argument) self
に対応する 実引数が記述されていない ことが原因であることがわかります。また、このようなエラーが発生した原因は以下の通りです。
-
Marubatsu
クラスのdraw_board
内では、update_widgets_status
は、ローカル関数として定義 されていたので、update_widgets_status()
のように記述して呼び出していた -
Marubatsu_GUI
クラスでは、先程update_widgets_status
を メソッドとして定義 したので、呼び出す際には、self.update_widgets_status()
のように、Marubatsu_GUI
クラスの インスタンスから呼び出す必要がある
従って、draw_board
を下記のプログラムの 3 行目のように修正することで、この問題を解決することができます。
def draw_board(self, ax, ai):
元と同じなので省略
self.update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
プログラム全体
def draw_board(self, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.mb.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.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.mb.BOARD_SIZE):
ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.mb.BOARD_SIZE):
for x in range(self.mb.BOARD_SIZE):
color = "red" if (x, y) == self.mb.last_move else "black"
self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)
self.update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
- update_widgets_status()
+ self.update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
上記の修正後に、下記のプログラムを実行すると、実行結果のように、別のエラーが発生します。このエラーの原因について少し考えてみて下さい。なお、表示されるボタンや画像は先ほどと同じなので省略します。
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
実行結果
Cell In[1], line 14
12 def update_widgets_status(self):
13 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
---> 14 set_button_status(self.first_button, self.move_count <= 0)
15 set_button_status(self.prev_button, self.move_count <= 0)
16 set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
AttributeError: 'Marubatsu_GUI' object has no attribute 'move_count'
新たなエラーの検証と修正
エラーメッセージから、update_widgets_status
メソッド内で、self.move_count
の処理を行う際に、Marubatsu_GUI
クラスのインスタンスである self
に move_count
という属性が存在していない ことが原因であることがわかります。
Marubatsu
クラスの draw_board
メソッド内で ローカル関数 として update_widgets_status
を定義 していた際には、self
は Marubatsu
クラスのインスタンス であったので、self.move_count
はエラーにはなりませんでした。
一方、Marubatsu_GUI
クラスの メソッド として update_widgets_status
を定義 した の場合は、self
は Marubatsu_GUI
のインスタンス を表します。
このように、update_widgets_status
内の self
の意味が異なる にも関わらず、draw_board
メソッドの ローカル関数 を そのままコピーして update_widgets_status
を定義 してしまったことがこの問題の原因です。
このエラーの原因も、先程と同様に、self
の意味が異なる点です。
ところで、self
の意味が変化したにもかかわらず、update_widgets_status
内の self.first_button
の部分でエラーが発生しない点が気になった人はいないでしょうか?Marubatsu_GUI
クラスを作成する前は、< ボタンのウィジェットは play
メソッド内の下記のプログラムで、Marubatsu
クラスの属性に代入 していました。そのため、self
に Marubatsu
クラスのインスタンス が代入されたローカル関数の update_widgets_status
内では self.first_button
に < ボタンのウィジェットが代入 されています。
self.first_button = create_button("<<", 100)
一方、Marubatsu_GUI
クラスでは、< ボタンのウィジェットは create_widgets
メソッド内の上記のプログラムで、Marubatsu_GUI
クラスの属性に代入 しています。そのため、self
に Marubatsu_GUI
クラスのインスタンス が代入された update_widgets_status
メソッド内でも self.first_button
に < ボタンのウィジェットが代入 されています。これが、self.first_button
のほうではエラーが発生しない理由です。
上記をまとめると、< ボタンのウィジェットの 代入先 を Marubatsu
クラスの属性から、Marubatsu_GUI
クラスの 属性に変更 したため、update_widgets_status
の self
の意味が変わっても self.first_button
に < ボタンのウィジェットが代入されるということです。
非常にわかりづらいと思いますので、Marubatsu
クラスと Marubatsu_GUI
クラスでの、update_widgets_status
の性質の違いを表にまとめます。
Marubatsu クラス |
Marubatsu_GUI クラス |
|
---|---|---|
定義 |
draw_board のローカル関数として定義 |
メソッドとして定義 |
self の意味 |
Marubatsu クラスのインスタンス |
Marubatsu_GUI クラスのインスタンス |
ボタンの代入先 |
Marubatsu クラスの属性 |
Marubatsu_GUI クラスの属性 |
move_count の代入先 |
Marubatsu クラスの属性 |
Marubatsu クラスの属性 |
上記の表からわかるように、Marubatsu_GUI
クラスの update_widgets_status
メソッド内では、ボタンの代入先と、move_count
などの代入先は、異なるクラスの属性に代入 されています。それにも関わらず、どちらも self.first_button
や self.move_count
のように、Marubatsu_GUI
クラスの属性としてそれらを記述 している点がエラーの原因です。
従って、この問題は下記のプログラムのように、update_widgets_status
を修正することで解決することができます。
-
4 ~ 7 行目:
Marubatsu
クラスの属性を参照する必要があるmove_count
とboard_records
の先頭をself.
からself.mb.
に修正する
1 # ウィジェットの状態を更新する
2 def update_widgets_status(self):
3 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
4 set_button_status(self.first_button, self.mb.move_count <= 0)
5 set_button_status(self.prev_button, self.mb.move_count <= 0)
6 set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
7 set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
8
9 Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
# ウィジェットの状態を更新する
def update_widgets_status(self):
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
set_button_status(self.first_button, self.mb.move_count <= 0)
set_button_status(self.prev_button, self.mb.move_count <= 0)
set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
# ウィジェットの状態を更新する
def update_widgets_status(self):
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
- set_button_status(self.first_button, self.move_count <= 0)
+ set_button_status(self.first_button, self.mb.move_count <= 0)
- set_button_status(self.prev_button, self.move_count <= 0)
+ set_button_status(self.prev_button, self.mb.move_count <= 0)
- set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
+ set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
- set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
+ set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
Marubatsu_GUI.update_widgets_status = update_widgets_status
上記の修正後に、下記のプログラムを実行するとエラーが発生しなくなり、実行結果のようにリプレイ機能に関するボタンが灰色で表示されて操作できなくなることが確認できます。
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
実行結果(下図は、画像なので操作することはできません)
draw_board
メソッドの改良
draw_board
メソッドを呼び出す際に、下記のプログラムのように、実引数に mb_gui.ax
、mb_gui.ai
を記述 していますが、draw_board
の仮引数 self
に mb_gui
が代入される ので、mb_gui.ax
や mb_gui.ai
の値は、draw_board
の中で self.ax
と self.ai
と記述して参照することができます。そのため、draw_board
の仮引数 ax
と ai
は必要がありません。
mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
そこで、draw_board
から 仮引数 ax
と ai
を削除する ことにします。その際に、draw_board
内の ax
と ai
を self.ax
と self.ai
に修正 する必要がありますが、仮引数 ax
と ai
を削除した後 で シンボル名の変更 で ax
を self.ax
に変更 しようとしても、下図のような表示が行われて 変更することができません。
これは、ax
を仮引数から削除 したことによって、ax
が ローカル変数として定義されていないことになる ためです。この問題を解決する方法の一つは、下記のプログラムの 2、3 行目ように、ax
と ai
にそれぞれ self.ax
と self.ai
を代入する処理を記述 するというものです。このように修正することで、削除した 仮引数 ax
と ai
と同じ名前のローカル変数にそれぞれの値が代入 されるので、以後のプログラムを変更する必要が無くなります。
1 def draw_board(self):
2 ax = self.ax
3 ai = self.ai
4
5 # Axes の内容をクリアして、これまでの描画内容を削除する
6 ax.clear()
元と同じなので省略
7
8 Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
def draw_board(self):
ax = self.ax
ai = self.ai
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.mb.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.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.mb.BOARD_SIZE):
ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.mb.BOARD_SIZE):
for x in range(self.mb.BOARD_SIZE):
color = "red" if (x, y) == self.mb.last_move else "black"
self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)
self.update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
修正箇所
-def draw_board(self, ax, ai):
+def draw_board(self):
+ ax = self.ax
+ ai = self.ai
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
元と同じなので省略
Marubatsu_GUI.draw_board = draw_board
先程のノートで説明したように、上記の修正を行った後で、シンボル名の変更を使って ax
と ai
を self.ax
と self.ai
に変更し、その後で、2、3 行目の代入文を削除するという方法で、ax
と ai
を self.ax
と self.ai
に変更することができます。
draw_board
の処理の中で、ai
と ax
に何らかの値を代入して変更する処理を行う必要がある場合は、ax
と self.ax
の中身が別々のものになるため ax
と ai
を self.ax
と self.ai
に変更する必要がありますが、実際にはそのような処理は行われないので、わざわざそのような作業を行うメリットはあまりないでしょう。
実行結果は省略しますが、上記の修正後に、下記のプログラムを実行して正しい処理が行われることを確認して下さい。
-
2 行目:
draw_board
の実引数の記述を削除する
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
mb_gui.draw_board()
修正箇所
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
-mb_gui.draw_board(mb_gui.ax, mb_gui.ai)
+mb_gui.draw_board()
以上が、Marubatsu_GUI
クラスでの draw_board
メソッドの定義です。self
に関するバグは初心者にはわかりづらいかもしれませんが、よくあるバグの原因の一つです。
self
を記述する場合は、self
が具体的に何を意味しているかを意識しながらプログラムを記述することを心掛けて下さい。
play
メソッドの修正
まだイベントハンドラに関する処理を記述していませんが、GUI に関する描画をできるようになったので、play
メソッドの中で Marubatsu_GUI
を利用する ように修正します。
下記は、そのように play
メソッドを修正したプログラムです。GUI に関する処理は、Marubatsu_GUI
の中ですべて行うことにしたので 非常にシンプルなプログラムになります。
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
上記の修正後に、下記のプログラムで、gui_play
を実行すると、ボタンとゲーム盤を描画するための Figure が表示されますが、実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。
from util import gui_play
gui_play()
実行結果(下図は、画像なので操作することはできません)
略
Cell In[14], line 11
8 Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
10 self.restart()
---> 11 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
NameError: name 'ax' is not defined
エラーの検証と修正
エラーメッセージから、play_loop
を呼び出す際に 実引数に記述した ax
が定義されていない ことがわかります。ax
は、Marubatsu_GUI
クラスのインスタンスの 属性に代入 されているので、下記のプログラムの 3 行目のように、Marubatsu_GUI
のインスタンスを mb_gui
に代入し、play_loop
の実引数 にキーワード引数 ax=mb_gui.ax
を記述 することでこの問題を解決することができます。
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
return self.play_loop(ai=ai, ax=mb_gui.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):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
self.restart()
return self.play_loop(ai=ai, ax=mb_gui.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):
元と同じなので省略
- return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+ return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
上記の修正後に、下記のプログラムで gui_play
を実行すると、実行結果のような別のエラーが発生します。このエラーの原因について少し考えてみて下さい。
gui_play()
実行結果
略
File c:\Users\ys\ai\marubatsu\082\marubatsu.py:682, in Marubatsu.draw_board.<locals>.update_widgets_status()
680 def update_widgets_status():
681 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
--> 682 set_button_status(self.first_button, self.move_count <= 0)
683 set_button_status(self.prev_button, self.move_count <= 0)
684 set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
AttributeError: 'Marubatsu' object has no attribute 'first_button'
新たなエラーの検証と修正
エラーメッセージから、update_widgets_status
内でエラーが発生 していることがわかりますが、よく見るとエラーが発生している下記の行の内容が、先程定義した Marubatsu_GUI
のメソッドとは異なる ことがわかります。
# エラーメッセージ内のエラーが発生している行
set_button_status(self.first_button, self.move_count <= 0)
# Marubatsu_GUI クラスの update_widgets_status の該当する行の記述
set_button_status(self.first_button, self.mb.move_count <= 0)
また、エラーメッセージの in Marubatsu.draw_board.<locals>.update_widgets_status()
から、エラーが発生した update_widgets_status
が Marubatsu
クラスの draw_board
内で定義されたローカル関数 であることがわかります。従って、このエラーは、play_loop
の中 で、古い Marubatsu
クラスの draw_board
を呼び出している ことが原因なので、新しい Marubatsu_GUI
の draw_board
メソッドを呼び出す ように修正することで解決できます。
play_loop
内で draw_board
は下記のプログラムのように記述して呼び出されています。
self.draw_board(ax, ai)
この self
は Marubatsu
クラスのインスタンス なので、Marubatsu_GUI
の draw_board
を呼び出すように修正するためには、Marubatsu_GUI
クラスのインスタンスの情報が必要 になりますが、その情報は現状では play_loop
メソッド内では利用できません。そこで、下記のプログラムの 4 行目のように、play
メソッド内で、Marubatsu_GUI
のインスタンスを作成した際 に、Marubatsu
クラスの mb_gui
という属性に代入 することにします。
-
4、7 行目:
mb_gui
をself.mb_gui
に修正する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3 if gui:
4 self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
5
6 self.restart()
7 return self.play_loop(ai=ai, ax=self.mb_gui.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):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
self.restart()
return self.play_loop(ai=ai, ax=self.mb_gui.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 の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
- Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
+ self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
self.restart()
- return self.play_loop(ai=ai, ax=mb_gui.ax, params=params, verbose=verbose, gui=gui)
+ return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
上記のように修正することで、下記のプログラムのように、play_loop
内で Marubatsu_GUI
クラスの draw_board
メソッドを利用できるようになります。
-
4、6 行目:
Marubatsu_GUI
クラスのdraw_board
メソッドを呼び出すように修正する
1 def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
2 # AI どうしの対戦の場合は画面を描画しない
3 if ai[0] is None or ai[1] is None:
4 self.mb_gui.draw_board()
元と同じなので省略
5 if gui:
6 self.mb_gui.draw_board()
元と同じなので省略
7
8 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.mb_gui.draw_board()
# 手番を人間が担当する場合は、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.mb_gui.draw_board()
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略
# AI どうしの対戦の場合は画面を描画しない
if ai[0] is None or ai[1] is None:
- self.draw_board(ax, ai)
+ self.mb_gui.draw_board()
元と同じなので省略
if gui:
- self.draw_board(ax, ai)
+ self.mb_gui.draw_board()
元と同じなので省略
Marubatsu.play_loop = play_loop
実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play
を実行して正しい処理が行われることを確認して下さい。
gui_play()
必要のない仮引数の削除
VSCode で play_loop
の定義を見ると下図のように 仮引数 ax
が薄い色で表示 されます。
VSCode では、「定義されているが、その後一度も利用されない名前」がこのように 薄い色で表示 されます。そのような名前はエラーではありませんが、その名前はその後で使われていないので、必要がない可能性が高い ことを意味します。実際に ax
は、先程の修正で self.draw_board(ax, ai)
を self.mb_gui.draw_board()
に修正した結果、draw_board
のブロックの内で一度も利用されなくなる ので、削除することができます。そこで下記のプログラムの 1 行目のように、play_loop
から 仮引数 ax
を削除することにします。
def play_loop(self, ai, params, verbose, gui):
元と同じなので省略
Marubatsu.play_loop = play_loop
プログラム全体
def play_loop(self, ai, 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.mb_gui.draw_board()
# 手番を人間が担当する場合は、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.mb_gui.draw_board()
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
-def play_loop(self, ai, ax, params, verbose, gui):
+def play_loop(self, ai, params, verbose, gui):
元と同じなので省略
Marubatsu.play_loop = play_loop
play_loop
の仮引数を修正したので、play
メソッド内で play_loop
を呼び出す処理を下記のプログラムの 3 行目のように修正する必要があります。
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
return self.play_loop(ai=ai, 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):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
self.restart()
return self.play_loop(ai=ai, 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):
元と同じなので省略
- return self.play_loop(ai=ai, ax=self.mb_gui.ax, params=params, verbose=verbose, gui=gui)
+ return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play
を実行して正しい処理が行われることを確認して下さい。
gui_play()
他の play_loop
の仮引数の削除
以下のように思った人はいないでしょうか?
-
ai
はMarubatsu_GUI
の属性に代入されるので、play_loop
の仮引数ai
がなくてもplay_loop
のブロックの中ではself.mb_gui.ai
と記述することで参照できる - 同様の理由で
play_loop
の仮引数params
がなくてもself.mb_gui.params
と記述することで参照できる - 従って、
ax
と同様に、ai
とparams
をplay_loop
の仮引数から削除できる
しかし、残念ながら下記の理由から、ai
や params
を play_loop
の仮引数から削除することはできません。
-
Marubatsu_GUI
のインスタンス は、play
メソッドの中で、GUI で〇×ゲームを遊ぶ場合でしか作られない -
CUI でゲームを遊ぶ場合 でも
play_loop
は呼び出されるが、その場合はself.mb_gui
には何も代入されていない ので利用できない
しかし、別の方法で play_loop
の仮引数を削除することができます。現状では、play_loop
には ai
、params
、verbose
、gui
の 4 つの仮引数がありますが、これらを Marubatsu_GUI
ではなく、Marubatsu
クラスのインスタンスの属性に代入 することで、play_loop
の仮引数を削除 することができます。そこで、そのように play
メソッドを修正することにします。
-
3 ~ 6 行目:
play_loop
の実引数に記述するデータをMarubatsu
クラスのインスタンスの属性に代入する -
7 行目:
play_loop
の実引数を削除する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
2 # 一部の仮引数をインスタンスの属性に代入する
3 self.ai = ai
4 self.params = params
5 self.verbose = verbose
6 self.gui = gui
元と同じなので省略
7 return self.play_loop()
8
9 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# 一部の仮引数をインスタンスの属性に代入する
self.ai = ai
self.params = params
self.verbose = verbose
self.gui = gui
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
self.restart()
return self.play_loop()
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# 一部の仮引数をインスタンスの属性に代入する
+ self.ai = ai
+ self.params = params
+ self.verbose = verbose
+ self.gui = gui
元と同じなので省略
- return self.play_loop(ai=ai, params=params, verbose=verbose, gui=gui)
+ return self.play_loop()
Marubatsu.play = play
次に、play_loop
を下記のプログラムのように修正します。
-
1 行目:
self
以外の仮引数を削除する - 2 ~ 5 行目:削除した仮引数と同じ名前のローカル変数に、対応する属性を代入する
1 def play_loop(self):
2 ai = self.ai
3 params = self.params
4 verbose = self.verbose
5 gui = self.gui
元と同じなので省略
6
7 Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self):
ai = self.ai
params = self.params
verbose = self.verbose
gui = self.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.mb_gui.draw_board()
# 手番を人間が担当する場合は、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.mb_gui.draw_board()
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
-def play_loop(self, ai, params, verbose, gui):
+def play_loop(self):
+ ai = self.ai
+ params = self.params
+ verbose = self.verbose
+ gui = self.gui
元と同じなので省略
Marubatsu.play_loop = play_loop
シンボル名の変更を使って ai
などを ai.self
に変更することは可能ですが、面倒なだけで大きなメリットはないので本記事では採用しないことにします。
重複する属性の削除
上記で、Marubatsu
クラスのインスタンスに ai
、params
、verbose
、gui
の 4 つの属性を追加しましたが、上記の中の ai
と params
の値は、Marubatsu_GUI
クラスの __init__
メソッドの中で、下記のプログラムのように Marubatsu_GUI
クラスのインスタンスの属性にも代入しています。
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
self.mb = mb
self.ai = ai
self.ai_dict = ai_dict
self.ai_params = params
self.size = size
以下略
同じデータ を Marubatsu
と Marubatsu_GUI
の両方のインスタンスに 重複して代入するのは無駄 なので、ai
と params
は Marubatsu
クラスの インスタンスの属性のみに代入する ことにします。まず、Marubatsu_GUI
クラスの __init__
メソッドを下記のプログラムのように修正します。なお、ai_dict
と size
は、GUI で〇×ゲームを遊ぶ際でのみ必要となる データなので、これまで通り Marubatsu_GUI
の属性に代入する ことにします。
-
1 行目:仮引数
ai
とparams
を削除する -
ai
とparams
に関する処理を削除する
1 def __init__(self, mb, ai_dict=None, size=3):
2 # ai_dict が None の場合は、空の list で置き換える
3 if ai_dict is None:
4 ai_dict = {}
5
6 self.mb = mb
7 self.ai_dict = ai_dict
8 self.size = size
元と同じなので省略
9
10 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
def __init__(self, mb, ai_dict=None, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
self.mb = mb
self.ai_dict = ai_dict
self.size = size
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
self.create_widgets()
self.display_widgets()
Marubatsu_GUI.__init__ = __init__
修正箇所
-def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
+def __init__(self, mb, ai_dict=None, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
- if params is None:
- params = [{}, {}]
self.mb = mb
- self.ai = ai
self.ai_dict = ai_dict
- self.ai_params = params
self.size = size
元と同じなので省略
Marubatsu_GUI.__init__ = __init__
次に、play
メソッドを下記のプログラムのように修正します。
-
3、4 行目:
__init__
メソッド内に記述されていた、params
がNone
の場合の処理を記述する2 -
10 行目:
Marubatsu_GUI
の実引数からai
とparams
を削除する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
2 # params が None の場合のデフォルト値を設定する
3 if params is None:
4 params = [{}, {}]
5
6 # 一部の仮引数をインスタンスの属性に代入する
7 self.ai = ai
元と同じなので省略
8 # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
9 if gui:
10 self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
元と同じなので省略
11
12 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# 一部の仮引数をインスタンスの属性に代入する
self.ai = ai
self.params = params
self.verbose = verbose
self.gui = gui
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
self.restart()
return self.play_loop()
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# params が None の場合のデフォルト値を設定する
+ if params is None:
+ params = [{}, {}]
# 一部の仮引数をインスタンスの属性に代入する
self.ai = ai
元と同じなので省略
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
- self.mb_gui = Marubatsu_GUI(self, ai=ai, ai_dict=ai_dict, params=params, size=size)
+ self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
元と同じなので省略
Marubatsu.play = play
上記の修正後に、下記のプログラムで、gui_play
を実行すると、実行結果のようなエラーが発生します。エラーの原因について少し考えてみて下さい。
gui_play()
実行結果
略
File c:\Users\ys\ai\marubatsu\082\marubatsu.py:770, in Marubatsu_GUI.create_dropdown(self)
767 # ai に代入されている内容を ai_dict に追加する
768 for i in range(2):
769 # ラベルと項目の値を計算する
--> 770 if self.ai[i] is None:
771 label = "人間"
772 value = "人間"
AttributeError: 'Marubatsu_GUI' object has no attribute 'ai'
エラーの検証と修正
エラーメッセージから、create_dropdown
内の self.ai[i]
で、Marubatsu_GUI
クラスのインスタンスに ai
が存在しないことが原因であることが分かります。これは、先程 ai
のデータを Marubatsu_GUI
クラスの属性に代入しないようにした ことが原因です。
従って、このエラーは、Marubatsu_GUI
の中で ai
属性を利用する処理を、self.ai
から self.mb.ai
に修正することで解決できます。
まず、create_dropwodn
メソッドを下記のプログラムのように修正します。
-
7、11、12 行目:
self.ai
をself.mb.ai
に修正します
1 import ipywidgets as widgets
2
3 def create_dropdown(self):
元と同じなので省略
4 # ai に代入されている内容を ai_dict に追加する
5 for i in range(2):
6 # ラベルと項目の値を計算する
7 if self.mb.ai[i] is None:
8 label = "人間"
9 value = "人間"
10 else:
11 label = self.mb.ai[i].__name__
12 value = self.mb.ai[i]
元と同じなので省略
13
14 Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
import ipywidgets as widgets
def create_dropdown(self):
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
self.dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if self.mb.ai[i] is None:
label = "人間"
value = "人間"
else:
label = self.mb.ai[i].__name__
value = self.mb.ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in self.ai_dict.values():
# 項目を登録する
self.ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
self.dropdown_list.append(
widgets.Dropdown(
options=self.ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
import ipywidgets as widgets
def create_dropdown(self):
元と同じなので省略
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
- if self.ai[i] is None:
+ if self.mb.ai[i] is None:
label = "人間"
value = "人間"
else:
- label = self.ai[i].__name__
+ label = self.mb.ai[i].__name__
- value = self.ai[i]
+ value = self.mb.ai[i]
元と同じなので省略
Marubatsu_GUI.create_dropdown = create_dropdown
ai
は draw_board
メソッドでも利用されているので、draw_board
メソッドを下記のプログラムの 3 行目のように修正する必要があります。
def draw_board(self):
ax = self.ax
ai = self.mb.ai
元と同じなので省略
Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
def draw_board(self):
ax = self.ax
ai = self.mb.ai
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.mb.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.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.mb.BOARD_SIZE):
ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.mb.BOARD_SIZE):
for x in range(self.mb.BOARD_SIZE):
color = "red" if (x, y) == self.mb.last_move else "black"
self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)
self.update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
修正箇所
def draw_board(self):
ax = self.ax
- ai = self.ai
+ ai = self.mb.ai
元と同じなので省略
Marubatsu_GUI.draw_board = draw_board
実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play
を実行して正しい処理が行われることを確認して下さい。
gui_play()
今回の記事のまとめ
今回の記事では、Marubatsu_GUI
クラスを定義 することで、Marubatsu
クラスから GUI の機能を分離 する作業の中で、ゲーム盤の描画を行う処理を実装しました。
その際に、同じ self
が異なる意味で使われることに由来する多くのエラー を紹介し、エラーの修正を行いました。
self
に限らず、名前の混同に関するエラー はよくあるエラーなので原因と対処方法についてしっかりと理解しておくことを強くお勧めします。
なお、現状ではまだイベントハンドラの機能を Marubatsu_GUI
に記述していないので、ゲームを遊ぶことはできません。また、現状のプログラムにはまだいくつかの問題があります。イベントハンドラの機能の実装と問題の修正は次回の記事で行います。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。なお、Marubatsu
クラスの draw_board
はもう必要がなくなったので削除しました。
次回の記事