目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
処理の統合
前回の記事で、リセットボタン を 実装 しましたが、play
、on_button_clicked
、on_mouse_down
の 中 で 似たような処理 が 記述 されているので、統合 することにします。ただし、3 つの関数 の 処理 は 似てはいます が、同じプログラム で 記述されていない ので、統合するため には 3 つの関数 で行われる 処理を検証 する 必要 が あります。
play
メソッドと on_button_clicked
の処理の統合
3 つの関数 の 統合 を 一度に行う のは 大変 なので、最初に play
メソッドと on_button_clicked
の 処理 の 統合 を行うことにします。
似たような処理 を 統合 するためには、具体的 な 共通点 を 調べる必要 があるので、play
メソッドと on_button_clicked
が 行う処理 を 整理 することにします。
play
メソッドが行う処理
play
メソッドが行う 処理 を 箇条書き にすると 以下 のようになります。
- 乱数の種 に 関する処理 を行う
- ゲーム を リセット する
- Figure に 関する処理 を行う
- リセットボタン に 関する処理 を行う
-
繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
- ゲーム盤の画像 を 描画 する
- 現在の手番 が AI の場合 は AI の着手 を 選択 する
- 現在の手番 が 人間 の場合 は、関数の処理 を 終了 して イベントループ を 再開 する
- 繰り返しの処理 の 終了後 に ゲーム盤の画像 を 描画 する
言葉の説明だけ では わかりづらい ので、フローチャート で 説明 します。
play
メソッドのフローチャートの整理
下図 は、以前の記事で紹介した play
メソッドの フローチャート です。
GUI で 〇×ゲーム を 遊ぶ場合 は、gui
に True
が 代入 されているので、その場合 の フローチャート は 下図 のようになります。なお、背景が 水色 の 繰り返し処理 の 中 の、「AI の手番?」の 条件分岐 は、その 右上 の「人間の手番?」が no(False)の 場合 に 実行される ので、必ず yes(True)になる点に 注意 して下さい。
上図 から、「gui
が True
」の 条件分岐 と、実行されない部分 を 削除 して 整理 すると、下図 のような フローチャート になります。なお、以前の記事 の フローチャート では、乱数の種 に関する 処理 が 省略 されていたので、下図 には その処理 を 追加 しました。また、以前の記事 でフローチャートを示した 時点 では、まだ リセットボタン に関する 処理 が 実装されていなかった ので、下図 には リセットボタン に関する 処理 を 追加 しました。
上図のフローチャートと、先程示した play
メソッドが行う 処理 を 見比べて みて下さい。
- 乱数の種 に 関する処理 を行う
- ゲーム を リセット する
- Figure に 関する処理 を行う
- リセットボタン に 関する処理 を行う
-
繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
- ゲーム盤の画像 を 描画 する
- 現在の手番 が AI の場合 は AI の着手 を 選択 する
- 現在の手番 が 人間 の場合 は、関数の処理 を 終了 して イベントループ を 再開 する
- 繰り返しの処理 の 終了後 に ゲーム盤の画像 を 描画 する
on_mouse_clicked
が行う処理
下記は、on_mouse_clicked
の定義です。
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.draw_board(ax)
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
else:
# 人間の手番の場合は、イベントハンドラを終了する
return
上記の on_mouse_clicked
行う 処理 を 箇条書き にすると 以下 のようになります。
- ゲーム を リセット する
- ゲーム盤の画像 を 描画 する
-
繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
- 現在の手番 が AI の場合 は AI の着手 を 選択 し、ゲーム盤の画像 を 描画 する
- 現在の手番 が 人間 の場合 は、関数の処理 を 終了 して イベントループ を 再開 する
前回の記事で示したように、上記 の処理の フローチャート は 下図 のようになります。
play
メソッドと on_button_clicked
の比較
下記は、play
メソッドと on_button_clicked
の 処理の手順 です。それぞれを 見比べて、どこ が 共通 し、どこが 異なるか について少し考えてみて下さい。
play
メソッドの処理の手順
- 乱数の種 に 関する処理 を行う
- ゲーム を リセット する
- Figure に 関する処理 を行う
- リセットボタン に 関する処理 を行う
-
繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
- ゲーム盤の画像 を 描画 する
- 現在の手番 が AI の場合 は AI の着手 を 選択 する
- 現在の手番 が 人間 の場合 は、関数の処理 を 終了 して イベントループ を 再開 する
- 繰り返しの処理 の 終了後 に ゲーム盤の画像 を 描画 する
on_button_clicked
の処理の手順
- ゲーム を リセット する
- ゲーム盤の画像 を 描画 する
-
繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
- 現在の手番 が AI の場合 は AI の着手 を 選択 し、ゲーム盤の画像 を 描画 する
- 現在の手番 が 人間 の場合 は、関数の処理 を 終了 して イベントループ を 再開 する
フローチャートの比較
わかりづらい と思った方は、下図の、play
メソッドと on_button_clicked
の フローチャート を横に並べて 比較 してみると良いでしょう。行われる処理 が 良く似ている ことが わかる のではないかと思います。なお、play
メソッドの 処理の説明文 の 一部 は、on_button_clicked
の 表記 に 合わせました。
処理の共通点と違い
上記から、それぞれの処理 には 以下 の 共通点 があることが わかります。
- ゲーム盤 を 最初 に リセット する
- 決着がつくまで、繰り返し処理 を行う
- 繰り返し 処理の 中 で、AI の手番 の場合は AI が 着手を行う
- 繰り返し 処理の 中 で、人間の手番 の場合は 関数の処理 を 終了 する
また、以下 の 違い があることが わかります。ただし、GUI で 〇×ゲーム を 遊ぶ場合 は、play
メソッドの 返り値 は 利用しない ので、返り値の違い は 重要 では ありません。
-
play
メソッドは、乱数の種 の 処理 と、Figure と リセットボタン に関する 処理を行う - 画像 を 描画 する タイミング が 異なる
-
play
メソッドは 繰り返し処理 の 終了時 に 返り値 として 勝敗結果 を返す
上記の「画像 を 描画 する タイミング が 異なる」という 違い から、一見すると 両者は 異なる処理 を行っているように 見えるかもしれません が、画像の描画 に関しては、play
メソッドと on_button_clicked
は 全く同じ処理 を行います。そのことを示します。
画像の描画の処理の検証
〇×ゲーム で ゲーム盤の描画 を 行う必要 が 生じる のは、下記 の 場合 です。
- ゲーム を 開始した時
- 着手 を 行った時
また、GUI で 〇×ゲーム を 遊ぶ 際に、人間 が 着手 を行った場合は、on_mouse_down
の イベントハンドラ内 で ゲーム盤の描画 を 行います。そのため、play
メソッドと on_button_clicked
では、AI の着手 に対してのみ 画像の描画 が 行われます。
play
メソッドと on_button_clicked
は、上記 の処理を、下記 の タイミング で 行います。なお、下記の「最後の着手」とは、ゲーム の 決着がつく着手 の事を 表します。
play |
on_button_clicked |
|
---|---|---|
ゲームを開始した時 | 最初 の 繰り返し 処理 の 直後 | 繰り返し 処理の 直前 |
最後以外の AI の着手 | 次 の 繰り返し 処理の 直後 | 着手 を行った 直後 |
最後の AI の着手 | 繰り返し 処理の 終了後 | 着手 を行った 直後 |
わかりづらいと思いますので、フローチャート で 説明 します。
ゲームを開始した時の画像の描画のタイミング
下図 は ゲーム を 開始した時 の 画像の描画 が行われる タイミング を表します。赤線 が 画像 が 描画されるまで に行われた 処理、黄色の処理 が 画像 の 描画処理 を表します。
ゲーム開始時 の時点では 決着 は ついていない ので、いずれの場合 も、必ずゲーム盤 の 描画 に関しては 同じ処理 が 行われる ことが 確認 できます。
AI が最初に着手を行った時の画像の描画のタイミング
下図は、上記の後 で AI が 着手した場合 に 画像の描画 が行われる タイミング を表します。水色の線 は、AI が 着手を選択 した 回 の 次 の 繰り返し 処理で 行われた処理 であることを表します。なお、AI の着手 は、最後の着手ではない ものとします。
図 からわかるように、play
メソッドと on_button_clicked
では、下記 のように 画像 を 描画 する タイミング が 異なります が、いずれ も 着手 を 行った後 で、次 の「AI の手番?」の 判定 を 行う前 に ゲーム盤の描画 が 行われる ことが わかります。
play |
on_button_clicked |
---|---|
次 の 繰り返し 処理の 直後 | その回 の 繰り返し 処理で 着手 を行った 直後 |
AI が 2 手目以降に着手を行った時の画像の描画のタイミング
下図は、AI が 2 手目以降 に 着手した場合 に 画像の描画 が行われる タイミング を表します。緑の線 は、AI が 着手を選択 した 回 の 前 の 繰り返し 処理で 行われた処理 であることを表します。なお、AI の着手 は、最後の着手ではない ものとします。
play
メソッドの 図 は、先程の図 と 全く同じ です。また、on_button_clicked
も、AI が 着手 を 選択した直後 に ゲーム盤の画像 を 描画 している点では 先程と同じ です。従って、先程と同様に、play
メソッドと on_button_clicked
では、下記 のタイミングで 画像 を 描画 し、いずれ も 着手 を 行った後 で、次 の「AI の手番?」の 判定 を 行う前 に ゲーム盤の描画 が 行われる ことが わかります。
play |
on_button_clicked |
---|---|
次 の 繰り返し 処理の 直後 | その回 の 繰り返し 処理で 着手 を行った 直後 |
人間の手番が回ってきた場合の処理
下図は、人間の手番 が 回ってきた場合 に行われる 処理 です。人間の手番 が 1 手目 の場合と 2 手目以降 の場合では、処理の流れ が 若干異なり ますが、本質的 には 変わらない ので、2 手目以降 に 着手した場合 に 画像の描画 が行われる タイミング を示します。
いずれの場合 も ゲーム盤の描画 を 行わず に 関数の処理 が 終了 することが わかります。
AI が最後の着手を行った時の画像の描画のタイミング
下図は、AI が 最後の着手 を 行った場合 に 画像の描画 が行われる タイミング を表します。今回 の図は、画像の描画 が 行われた後 で、関数 の 処理が終了するまで の 流れ を 示しました。線の色 は、緑、赤、青 の 順 で 処理が行われる ことを 表します。
図 から、play
メソッドは、繰り返し の 処理 の 終了後 に ゲーム盤の画像 を 描画 しますが、on_button_clicked
では、これまでと同様 に、着手 を 行った直後 に 描画 します。いずれの場合 も 関数の処理 が 終了するまで の間に ゲーム盤 を 描画 することが わかります。
play |
on_button_clicked |
---|---|
繰り返し 処理が 終了 した 直後 | その回 の 繰り返し 処理で 着手 を行った 直後 |
まとめ
上記 から、いずれの場合 でも、play
メソッドと on_button_clicked
は ゲームの開始時 と、AI が 着手を行った後 に ゲーム盤 の 画像を描画する という、同じ処理 を 行う ことが 確認 できました。下記は、両者 が 同じ処理を行う ことを 言葉で説明 したものです。
-
play
メソッドは、繰り返し 処理の 中 で、着手 を 行う前 に 画像を描画 する。そのため、ゲーム開始時 の 描画処理 を 繰り返し処理 の中で 行う ことが できる。一方、最後 の AI の着手 に対する 描画処理 は 繰り返し 処理の 中 で 行えない ので、繰り返し 処理の 終了後 に その処理 を 行う必要 がある -
on_button_clicked
は、繰り返し 処理の 中 で、着手 を行った 直後 に 画像を描画 する。そのため、着手 に対する 描画処理 を 繰り返し処理 の中で 行う ことが できる。一方、ゲーム開始時 の 描画処理 は 繰り返し 処理の 中 で 行えない ので、繰り返し 処理の 前 に その処理 を 行う必要 がある
play_common
の定義
上記の考察 から、play
メソッド と on_button_clicked
が 行う処理 の 違い は、play
メソッドが 繰り返し 処理の 前 に 行う「乱数の種 に 関する処理」と「Figure と リセットボタン に 関する処理」だけ である事が わかりました。
そこで、両者 で 共通する処理 を行う メソッドを定義 することにします。その メソッドの名前 は、共通 するという 意味 を表す common という 英単語 を使って、play_common
という 名前にする ことにします。
どちらの処理を play_common
に記述すべきか
次に、play_common
に 記述 する プログラム を、play
メソッドと on_button_clicked
の どちら の プログラム から 抜き出し て 記述 するかを 考える必要 があります。上記 で示したように、どちらも 同じ処理を行う のだから、どちらでも良い のではないかと 思う人 が いるかもしれません が、そうではありません。その理由について少し考えてみて下さい。
play
メソッドは、gui
に False
が 代入 されて 実行 される 場合 が あります。一方、on_button_clicked
は、gui
に True
が 代入 された場合に イベントハンドラ として 登録 されるため、gui
に True
が 代入 されている場合 のみ実行 されます。そのため、play
メソッドと on_button_clicked
の 両方 から play_common
メソッドを呼び出して 利用する場合 は、play
メソッドの 内容を記述 しなければ、play
メソッドで gui
に False
が 代入 されている場合の処理に 対応できない という 問題が発生 します。
そこで、play_common
を、play
メソッドから 「乱数の種 に 関する処理」と「Figure と リセットボタン に 関する処理」以外 の 処理 を 抜き出した、下記 のプログラムのように 定義 します。play_common
の 仮引数 は、とりあえず play
メソッドと 同じものにする ことにしました。その 理由 は この後 で 説明 します。
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import japanize_matplotlib
import ipywidgets as widgets
import math
def play_common(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
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_common = play_common
play_common
の不必要な仮引数の削除
上記 のプログラムを VSCode に 記述 すると、関数の定義 が、下図 のように 表示 されます。VSCode では、仮引数 が その関数 の ブロックの中 で一度も 利用されていない 場合は、薄い色の文字 で 表示 されます。下図 では、seed
と size
が 薄い色で表示 されます。
従って、play_common
の 仮引数 seed
と size
は 必要がない ことが わかります。実際 に、VSCode で Ctrl + f を押して表示される 検索機能 を使って、seed
と size
が play_common
の ブロックの中 で一度も 記述されていない ことを 確認 して下さい。
下記 のプログラムは、仮引数 seed
と size
を 削除 した play_common
の 定義 です。
def play_common(self, ai, params=[{}, {}], verbose=True, gui=False):
元と同じなので省略
Marubatsu.play_common = play_common
プログラム全体
def play_common(self, ai, params=[{}, {}], verbose=True, gui=False):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
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_common = play_common
修正箇所
-def play_common(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
+def play_common(self, ai, params=[{}, {}], verbose=True, gui=False):
元と同じなので省略
Marubatsu.play_common = play_common
play
メソッドと on_button_clicked
の修正
次に、下記 のプログラムのように、play
メソッドと on_button_clicked
から、共通する処理 を 削除 し、代わり に play_common
を 呼び出す ように 修正 します。
-
6 行目:
on_button_clicked
に、play_common
メソッドを 呼び出す処理のみ を 記述 する -
10 行目:ゲーム の 決着がついていない間 繰り返す 処理を削除 し、return 文 で、
play_common
メソッドの 返り値 を 返す処理 を 記述する
play
メソッドの場合は、勝敗結果 を 返す 必要がある1ので、return 文 で play_common
の 返り値 を 返す必要がある 点に 注意 して下さい。イベントハンドラ は 返り値 を 返す必要がない ので、on_button_clicked
では、return 文 を 記述する必要 は ありません。
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_button_clicked(b):
4 # この下に記述されていた処理を削除する
5
6 self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
元と同じなので省略
7
8 # この下に記述されていた、ゲームの決着がついていない間繰り返す処理以降を削除する
9
10 return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
11
12 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.play_common(ai=ai, 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)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
if self.status == Marubatsu.PLAYING and ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
- self.restart()
- self.draw_board(ax)
# 現在の手番を表す ai のインデックスを計算する
- index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
- if self.status == Marubatsu.PLAYING and ai[index] is not None:
- x, y = ai[index](self, **params[index])
- self.move(x, y)
- self.draw_board(ax)
+ self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
元と同じなので省略
# ゲームの決着がついていない間繰り返す
- while self.status == Marubatsu.PLAYING:
- 非常に長いのでこの間の部分は省略
- return self.status
+ return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のような、エラーが発生 します。このエラーの原因について少し考えてみて下さい。
%matplotlib widget
from ai import ai2
mb = Marubatsu()
mb.play(ai=[ai2, ai2], gui=True);
実行結果(下記のエラーメッセージの下に表示される画像は省略します)
略
Cell In[3], line 45
42 # fig の画像にマウスを押した際のイベントハンドラを結び付ける
43 fig.canvas.mpl_connect("button_press_event", on_mouse_down)
---> 45 return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
Cell In[2], line 49
47 if verbose:
48 if gui:
---> 49 self.draw_board(ax)
50 else:
51 print(self)
NameError: name 'ax' is not defined
エラーの検証と修正
エラーメッセージ から、ax
が 定義されていない(not defined)ことが わかります。また、エラーが発生 している self.draw_board(ax)
の 行 は、---> 45 return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
という メッセージ から、play_common
の中 で 実行 されていることが わかります。
従って、このエラー は、play_common
の中 で、ax
が 定義されていない ことが 原因 です。
上記 の 名前が定義されていない という エラー は、以前の記事で説明したように、下図 のように、VSCode の play_common
の ブロック内 のプログラムの ax
の部分に オレンジ色 の 波線 が 表示 されることから 気づく ことも できます。
そこで、下記 のプログラムのように、play_common
に 仮引数 ax
を 追加 します。なお、通常の仮引数 は、デフォルト引数より前 に 記述 する 必要がある 点に 注意 して下さい。
def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
元と同じなので省略
Marubatsu.play_common = play_common
プログラム全体
def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
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_common = play_common
修正箇所
-def play_common(self, ai, params=[{}, {}], verbose=True, gui=False):
+def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
元と同じなので省略
Marubatsu.play_common = play_common
次に、下記 のプログラムの 4、7 行目 のように、play
メソッドと on_button_clicked
内で play_common
を 呼び出す際 に、実引数 に ax=ax
を 記述 するように 修正 します。
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_button_clicked(b):
4 self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
5
元と同じなので省略
6
7 return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
8
9 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.play_common(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)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
if self.status == Marubatsu.PLAYING and ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
- self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
+ self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
- return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
+ return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 し、正しく プログラムが 動作する ことを 確認 して下さい。また、人間 VS 人間、AI VS 人間、人間 VS AI でも 正しく プログラムが 動作する ことを 確認 して下さい。
mb.play(ai=[ai2, ai2], gui=True);
play
メソッドと on_mouse_down
の処理の統合
次に、play
メソッドと on_mouse_down
の 処理 を 統合 することにします。
play
メソッドと on_mouse_down
の処理の共通点
on_mouse_down
が 行う処理 を 箇条書き にすると 以下 のようになります。
- マウス が 押されたマス に 着手 を行う
- 着手後の ゲーム盤 の 画像を描画 する
- ゲーム中 で 手番 が AI の場合 は、AI の着手 を行い、画像を描画 する
そのように思えないかもしれませんが、上記 の 手順 2、3 の 処理 は、下記 の play
メソッドの 手順 5、6 で 置き換える ことが できます。その理由について少し考えてみて下さい。
- 乱数の種 に 関する処理 を行う
- ゲーム を リセット する
- Figure に 関する処理 を行う
- リセットボタン に 関する処理 を行う
-
繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
- ゲーム盤の画像 を 描画 する
- 現在の手番 が AI の場合 は AI の着手 を 選択 する
- 現在の手番 が 人間 の場合 は、関数の処理 を 終了 して イベントループ を 再開 する
- 繰り返しの処理 の 終了後 に ゲーム盤の画像 を 描画 する
下図左 は on_mouse_down
の、下図右 は on_mouse_down
の 手順 2、3 を play
メソッドの 手順 5、6 で 置き換え た フローチャート です。緑 の部分が on_mouse_down
の、水色 の部分が play
メソッドの 手順 であることを表します。このフローチャート を使って、両者 が 同じ処理を行う ことを 示す ことにします。
on_mouse_down
が 実行された ということは、〇 か × の いずれか を 人間が担当 しているということなので、AI VS AI 以外 の 組み合わせ の対戦で 行われる処理 を 検証 します。
人間 VS 人間 の場合
on_mouse_down
の 手順 1 の 後 は、以下 の 2 種類の状況 が 考えられる ので、それぞれ の 処理の流れ が 同じ であることを 示します。
- 手順 1 で行われた 着手 によって 決着 が つく
- 手順 1 で行われた 着手 によって 決着 が つかない
下図 は、on_mouse_down
の 手順 1 によって、ゲーム の 決着 が ついた場合 の 処理 を表す フローチャート です。図から、いずれの場合 も 手順 1 によって行われた 人間 の 着手後 の ゲーム盤の描画 を行い、処理が終了 します。
下図 は、on_mouse_down
の 手順 1 によって、ゲーム の 決着 が ついていない場合 の 処理 を表す フローチャート です。下図左 で行われる 処理 は 上記 と 同じ です。下図右 で行われる 処理 は、処理の流れ は 変わっています が、人間 の 着手後 の ゲーム盤の描画 を行い、処理が終了する という点では両者は 同じ処理 を 行います。
上記 から、人間 VS 人間 の場合は、両者は 同じ処理を行う ことが 確認 できました。
人間 VS AI または、AI VS 人間 の場合
人間 VS AI と AI VS 人間 の いずれの場合 も、on_mouse_down
の 手順 1 は 人間の着手の処理 である点に 変わりはありません。そのため、両者 の 処理の手順 を まとめて説明します。
on_mouse_down
の 手順 1 の 後 は、以下 の 3 種類の状況 が 考えられる ので、それぞれ の 処理の流れ が 同じ であることを 示します。
- 手順 1 で行われた 着手 によって 決着 が つく
-
手順 1 で行われた 着手 によって 決着 が つかない
- 次 の AI の着手 によって 決着 が つく
- 次 の AI の着手 によって 決着 が つかない
手順 1 で行われた 着手 によって 決着 が つく 場合に 行われる処理 は、人間 VS 人間 の場合と 全く同じ なので説明は 省略 します。
下図 は、on_mouse_down
の 手順 1 によって、ゲーム の 決着 が つかず、その 次 の AI の着手 によって 決着 が ついた場合 の 処理 を表す フローチャート です。線の色 は、赤、水色 の 順 で 処理が行われる ことを 表します。
図から、いずれの場合 も 下記 の処理が 行われる ことが わかります。
- 人間 の 着手後 の ゲーム盤の描画 が 行われる
- AI が 着手 を行い、ゲーム盤の描画 が 行われる
下図 は、on_mouse_down
の 手順 1 によって、ゲーム の 決着 が つかず、その 次 の AI の着手 によって 決着 が つかなかった場合 の 処理 を表す フローチャート です。下図左 で行われる 処理 は 上記 と 同じ です。下図右 で行われる 処理 は、処理の流れ は 変わっています が、下記の処理 が 行われる という点では、両者は 同じ処理 を 行います。なお、下図右 の 黄色 の ゲーム盤を描画する 処理は、人間 の 着手後 と AI の 着手後 で 2 度実行 されます。
- 人間 の 着手後 の ゲーム盤の描画 が 行われる
- AI が 着手 を行い、ゲーム盤の描画 が 行われる
上記 から、人間 VS AI と AI VS 人間 の場合も、同じ処理を行う ことが 確認 できました。
play_loop
の定義
上記から、on_mouse_down
の 手順 2、3 と play
メソッドの 手順 5、6 は 同じ処理 を 行う ことが 確認 できました。そこで、play_common
の場合と 同様 に、共通する処理 である play
メソッドの 手順 5、6 の処理を 抜き出したメソッド を 定義 する事にします。
play
メソッドの 手順 5 の 処理 は、繰り返しの処理 なので、メソッド の 名前 を play_loop
と 名付ける ことにし、下記 のプログラムのように 定義 します。
def play_loop(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
# ゲームの決着がついていない間繰り返す
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
play_common
との違い
-def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
+def play_loop(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
# 〇×ゲームを再起動する
- self.restart()
元と同じなので省略
Marubatsu.play_loop = play_loop
on_mouse_down
の修正
次に、on_mouse_down
の、マウス を 押したマス に 着手を行った後 の 処理 を、下記 のプログラムのように、play_loop
を 呼び出す プログラムに 修正 します。
-
12 行目:AI が 着手を行う処理 を 削除 し、
play_loop
を 呼び出す 処理を 記述 する
1 def play(self, ai, params=[{}, {}], 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)
10
11 # 次の手番の処理を行うメソッドを呼び出す
12 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
13
元と同じなので省略
14
15 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.play_common(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)
return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], 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)
# 現在の手番を表す ai のインデックスを計算する
- index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
- if self.status == Marubatsu.PLAYING and ai[index] is not None:
- x, y = ai[index](self, **params[index])
- self.move(x, y)
- self.draw_board(ax)
# 次の手番の処理を行うメソッドを呼び出す
+ self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 し、正しく プログラムが 動作する ことを 確認 して下さい。また、人間 VS 人間、AI VS 人間、人間 VS AI でも 正しく プログラムが 動作する ことを 確認 して下さい。
mb.play(ai=[ai2, ai2], gui=True);
play_common
と play_loop
の統合
ここまでで、play
、on_button_clicked
、on_mouse_down
の 処理 の 統合 を 行うため に、play_loop
と play_common
の 2 種類 の メソッド を 定義 しましたが、この 2 つのメソッド で行われる 処理の違い は下記の点だけなので、統合 したほうが良いでしょう。
-
最初 に
self.restart()
を 実行 して ゲーム を リセットするか どうか
従って、play_common
の中で、play
メソッドの 手順 5、6 の処理を 下記 のプログラムの 6 行目 のように play_loop
を 呼び出す ように 修正 することが できます。
1 def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
2 # 〇×ゲームを再起動する
3 self.restart()
4
5 # 決着がつくまでの繰り返し処理を行うメソッドを呼び出す
6 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
7
8 Marubatsu.play_common = play_common
行番号のないプログラム
def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
# 〇×ゲームを再起動する
self.restart()
# 決着がつくまでの繰り返し処理を行うメソッドを呼び出す
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play_common = play_common
修正箇所
def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
# 〇×ゲームを再起動する
self.restart()
# 決着がつくまでの繰り返し処理を行うメソッドを呼び出す
+ self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
- # ゲームの決着がついていない間繰り返す
- while self.status == Marubatsu.PLAYING:
- 以下非常に長いので省略します
# 決着がついたので、ゲーム盤を表示する
if verbose:
if gui:
self.draw_board(ax)
else:
print(self)
return self.status
Marubatsu.play_common = play_common
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 し、正しく プログラムが 動作する ことを 確認 して下さい。また、人間 VS 人間、AI VS 人間、人間 VS AI でも 正しく プログラムが 動作する ことを 確認 して下さい。
mb.play(ai=[ai2, ai2], gui=True);
play_common
の削除
play_common
が 行う処理 は、self.restart
と play_loop
を 呼び出す という 処理だけ なので、わざわざ メソッドとして定義 する 必要はない と 思う人 が いるかもしれません。
そのように感じた人は、下記 のプログラムの 5 ~ 7、9 ~ 11 行目 のように、play
メソッドと on_button_clicked
内で play_common
を 呼び出す処理 を、self.restart
と play_loop
を呼び出す処理に 修正 すると良いでしょう。
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_button_clicked(b):
4 # ゲームをリセットする
5 self.restart()
6 # 決着がつくまで繰り返す処理を呼び出す
7 self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
8 # ゲームをリセットする
9 self.restart()
10 # 決着がつくまで繰り返す処理を呼び出す
11 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
12
13 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
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, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
# ゲームをリセットする
+ self.restart()
# 決着がつくまで繰り返す処理を呼び出す
- self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+ self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
# ゲームをリセットする
+ self.restart()
# 決着がつくまで繰り返す処理を呼び出す
- return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+ return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
本記事 では 上記 のプログラムを 採用 しますが、修正前 と 修正後 のプログラムで、どちらが良いか については 一概には言えません。わかりやすい と 思ったほう を 採用して下さい。なお、play_common
はもう 利用しない ので、marubatsu.py には 記述しません。
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 し、正しく プログラムが 動作する ことを 確認 して下さい。また、人間 VS 人間、AI VS 人間、人間 VS AI でも 正しく プログラムが 動作する ことを 確認 して下さい。
mb.play(ai=[ai2, ai2], gui=True);
今回の記事のまとめ
今回の記事では、play
、on_button_clicked
、on_mouse_down
の 処理 の 統合 を行いました。このような 処理の統合 を 行わなくても、リセットボタン の 処理を記述 することが できます が、統合したほう が、プログラム を 簡潔に記述できる ようになります。統合前 と 統合後 のプログラムを 見比べて、そのことを 確認 してみて下さい。
また、同じ処理 をプログラムの 複数の個所 に 記述する ことは、避けたほうが良い ので、このような 統合ができる場合 は、しておいたほうが良い でしょう。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事
-
play
メソッドの 返り値 は、ai_match
を使って 通算成績 を 計算 する際などで 必要 になります ↩