目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
GUI での AI との対戦
前回の記事では、人間どうし が GUI で 〇×ゲーム を 遊べる ようにしました。
今回の記事では GUI で AI の対戦 が できるよう にします。
AI の対戦 は、下記の 3 種類 があります。なお、下記 の表記は、VS の 前に記述 したものが 〇 を担当 するという 意味 を表します。それぞれ について 順番に実装 を行います。
- AI VS AI
- 人間 VS AI
- AI VS 人間
AI VS AI の対戦
まず、下記 のプログラムのように、現在 の play
メソッドで、実引数 に gui=True
を 記述 して、AI どうし の 対戦 を行ってみることにします。下記 は、ai2
VS ai2
の 対戦 です。
%matplotlib widget
from marubatsu import Marubatsu
from ai import ai2
mb = Marubatsu()
mb.play(ai=[ai2, ai2], gui=True)
実行結果
実行結果 から、ゲーム盤 は 表示されます が、AI の着手 が 行われない ことがわかります。また、ゲーム盤 の マスの上 で マウスを押す と、着手 を行うことが できてしまう ことから、AI どうし の 対戦 を行ったにも 関わらず、人間どうし の 対戦 が 行われてしまう という 問題が発生 しています。このようなことがおきる理由ついて少し考えてみて下さい。
問題の検証
play
メソッドは、下記 のプログラムの 8 行目 のように、gui
が True
の場合 は、手番を人間と AI のどちらが担当していた場合でも、ゲーム盤の画像 を 描画した後 で、return 文 を 実行 して play
メソッドの 処理を終了 します。また、その後 の 着手の処理 は、画像の上 で マウス が 押された 際に 実行 される、on_mouse_down
によって 行われる ので、人間 が 着手を行う ことが できてしまいます。これが 上記の問題 が発生する 原因 です。
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
中略
2 # ゲームの決着がついていない間繰り返す
3 while self.status == Marubatsu.PLAYING:
4 # ゲーム盤の表示
5 if verbose:
6 if gui:
7 self.draw_board(ax)
8 return
9 else:
10 print(self)
11 # 現在の手番を表す ai のインデックスを計算する
12 index = 0 if self.turn == Marubatsu.CIRCLE else 1
13 # ai が着手を行うかどうかを判定する
14 if ai[index] is not None:
15 x, y = ai[index](self, **params[index])
以下略
下記は、前回の記事で紹介した、gui
に True
が 代入 されていた場合の、play
メソッドの フローチャート です。図から、play
メソッドでは、AI が 〇 の手番 を 担当する場合 でも、図の中 にある「AI が 着手を選択する」という 処理 が、実行されない ことが わかります。
問題の修正
AI どうし の 対戦 の場合は、すべての着手 を AI が行う ので、on_mousedown
による、GUI の 入力の処理 は 必要 は ありません。従って、人間が着手 を 行う場合 のように、play
メソッドを 終了する必要 は ありません。
また、AI が 着手を行う処理 は、上記 のプログラムの 15 行目 で行われます。従って、AI が 着手を行う 場合は、上記 の 8 行目 の return 文 を 実行しない ようにすれば、15 行目 が 実行 されて AI が 着手を行う ようになります。
下記 は、そのように play
メソッドを 修正 したプログラムです。
-
8 行目:この後の 13 行目 で
index
を 利用 する 必要がある ので、元 は 17 行目の前 にあった、現在の手番 を表すai
の インデックス を 計算 する 処理 を ここに移動 する -
13 行目:現在の手番 が 人間の場合 に return 文 を 実行 して
play
メソッドを 終了 する
1 import matplotlib.pyplot as plt
2 import math
3
4 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
5 # ゲームの決着がついていない間繰り返す
6 while self.status == Marubatsu.PLAYING:
7 # 現在の手番を表す ai のインデックスを計算する
8 index = 0 if self.turn == Marubatsu.CIRCLE else 1
9 # ゲーム盤の表示
10 if verbose:
11 if gui:
12 self.draw_board(ax)
13 if ai[index] is None:
14 return
15 else:
16 print(self)
17 # ai が着手を行うかどうかを判定する
18 if ai[index] is not None:
19 x, y = ai[index](self, **params[index])
元と同じなので省略
20
21 Marubatsu.play = play
行番号のないプログラム
import matplotlib.pyplot as plt
import math
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# 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)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board(ax)
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 = play
修正箇所
import matplotlib.pyplot as plt
import math
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
+ index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board(ax)
- return
+ if ai[index] is None:
+ return
else:
print(self)
- index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self, **params[index])
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のように、AI どうし の 対戦 が行われ、その 結果の画像 が 表示 されます。また、下記のセル を 何度 か 実行し直し て、毎回異なる対戦結果 が 表示 されることを 確認 してみて下さい。
mb.play(ai=[ai2, ai2], gui=True)
実行結果(実行結果はランダムなので下記とは異なる場合があります)
AI どうしの対戦で行われる処理のフローチャート
下図左 は、gui
が True
の場合に、AI どうし の 対戦 が行われる際の、play
メソッドの フローチャート です。下図右 の、前回の記事で紹介した、gui
が False
の場合に行われる フロー駆動型 の play
メソッドの フローチャート と 見比べて 下さい。
Figure に関する処理 などの 細かい部分 で いくつかの違い が ありますが、行われる 処理の流れ が ほぼ同じ であることを 確認して下さい。従って、AI どうし の 対戦 では、フロー処理型 の場合と 同様 に、決着がつくまで の 処理 を play
メソッドで 行います。
ところで、上図左 の フローチャート から わかる ように、gui
に True
が 代入 されている場合は、play
メソッドは イベントハンドラ を 登録 するので、play
メソッドの 終了後 も、ゲーム盤の上 で マウスを押す と、登録 した イベントハンドラ が イベントループ から 呼び出されます。そのため、play
メソッドを 実行後 に ゲーム盤の上 で マウスを押す と、着手 が 行われてしまう のではないかと 思う人 が いるかもしれません。
実際 に、play
メソッドの 実行後 に ゲーム盤の上 で マウスを押す と イベントハンドラ が 実行 されますが、前回の記事で示した 下記 の フローチャート のように、ゲーム の 決着後 の 着手を禁止 する 処理 を イベントハンドラ に 記述 したので、着手 は 行われません。
無駄な描画の処理を行わないようにする
下図 は、gui
に True
が 代入 されていた場合に、play
メソッドで AI どうし の 対戦 を 行った場合 の フローチャート の 一部 です(繰り返し の 前の部分 を 省略 しています)。
図 の 黄色い長方形 が、draw_board
で ゲーム盤の画像 の 描画を更新 する処理ですが、図から わかるように、この処理は、以下 の タイミング で 何度も実行 されます。
- 繰り返し の処理の 中 で、決着がつくまで の それぞれの局面 を 描画 する
- 繰り返し の処理の 後 で、決着がついた局面 を 描画 する
以前の記事 で説明したように、matplotlib の 画像 の 描画の更新 は、JupyterLab の セル のプログラムの 実行 が 終了するまで は 行われない ので、JupyterLab 上の 画像 には、AI どうし の 対戦 の 途中経過 は 描画されず、決着後の局面 の ゲーム盤のみ が 描画 されます。
上記のことから、AI どうし の 対戦 の場合は、対戦の途中経過 を 描画 するために呼び出した draw_board
メソッドの 処理 は 無駄な処理 であるということが わかります。〇×ゲーム のような、最大 でも 9 手 で ゲームが終了 するようなゲームの場合は、決着 が ついていない間 の draw_board
の 呼び出し は 最大 でも 9 回 しか行われないので、play
メソッドの 処理が遅い と 感じる ことは ありません が、ゲーム の 決着がつくまで にもっと 長い手数がかかる ようなゲームの場合は、無駄 な draw_board
の 呼び出し によって、ゲーム の 決着がつくまで にかかる 時間 が 長く感じられる ような 場合 が 生じる可能性 があります。
そこで、下記 のプログラムのように、AI どうし の 対戦 で、ゲーム の 決着がついていない 場合は draw_board
メソッドを 呼び出さない ように 修正 することにします。
- 10 行目:「AI どうし の 対戦 の場合は 画像 を 描画しない」ということは、「どちらか が 人間 である場合に 画像を描画 する」ということなので、その条件式 を 記述 する
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # ゲームの決着がついていない間繰り返す
3 while self.status == Marubatsu.PLAYING:
4 # 現在の手番を表す ai のインデックスを計算する
5 index = 0 if self.turn == Marubatsu.CIRCLE else 1
6 # ゲーム盤の表示
7 if verbose:
8 if gui:
9 # AI どうしの対戦の場合は画面を描画しない
10 if ai[0] is None or ai[1] is None:
11 self.draw_board(ax)
12 # 手番を人間が担当する場合は、play メソッドを終了する
13 if ai[index] is None:
14 return
15 else:
16 print(self)
元と同じなので省略
17
18 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)
# 〇×ゲームを再起動する
self.restart()
# 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)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
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 = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲーム盤の表示
if verbose:
if gui:
# AI どうしの対戦の場合は画面を描画しない
- self.draw_board(ax)
+ if ai[0] is None or ai[1] is None:
+ self.draw_board(ax)
# 手番を人間が担当する場合は、play メソッドを終了する
if ai[index] is None:
return
else:
print(self)
元と同じなので省略
Marubatsu.play = play
10 行目 で、「AI どうし の 対戦 の場合は 画像 を 描画しない」という 条件式 を 記述しない理由 は、その条件式 は、下記 のような 複雑な条件式 になるからです。
if not((ai[0] is not None) and (ai[1] is not None)):
self.draw_board(ax)
実行結果は省略しますが、上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、AI どうし の 対戦 が行われ、その 結果の画像 が 表示 されることが 確認 できます。
mb.play(ai=[ai2, ai2], gui=True)
上記の修正 は、処理 の 無駄を省く ための修正なので、行わなくても プログラムは 正しく動作 します。また、この処理 を フローチャートに記述 すると、フローチャートが 複雑 になって わかりづらくなる ので、以後の play
メソッドの フローチャート を図で示す際に、この処理 は 省略 することにします。
なお、上記の修正 を行うことで、AI どうし の 対戦 を行った際の play
メソッドの 処理速度 は 実際 に 短く なりますが、先程説明したように、〇×ゲーム の場合は、決着がつくまで の 手数が短い ので、処理速度の差 を 人間が体感 することは できません。
以前の記事の プログラムのテスト の記事で説明したように、プログラムの修正 を行った場合は、新しい処理 が 正しく動作する ことを 確認 する だけでなく、これまで に 動作していた処理 が、引き続き 正しく動作するか どうかを 確認したほうが良い でしょう。そこで、下記 のプログラムで play
メソッドを 実行 して、人間 どうし の 対戦 を行う場合も、問題なく 対戦が 行える ことを 実際に確認 してみて下さい。
mb.play(ai=[None, None], gui=True)
人間 VS AI の対戦
先程と同様 に、play
メソッドの 実引数 に gui=True
を 記述 して、下記 のプログラムのように、人間 VS AI の 対戦 を 行ってみる ことにします。下記は、人間 VS ai2
の 対戦 です。
mb.play(ai=[None, ai2], gui=True)
実行結果
実行結果 のように、ゲーム盤が表示 されるので、好きなマス の上で マウスを押す とそのマスに 着手 が 行われます。下記 は、(0, 0) のマスの上で マウス を 押した場合 の 図 です。
上図 のように、人間 が 着手 を 行った後 で、AI の着手 は 行われません。また、上図 でゲーム盤の マスの上 で マウスを押す と、着手 を 行うこと が できてしまう ため、人間 VS AI の 対戦 を 行おうとした のにも 関わらず 、人間どうし の 対戦 が 行われてしまう という 問題が発生 しています。何故このようなことになるかについて少し考えてみて下さい。
問題の検証
人間 VS AI の 対戦 の場合は、gui
が True
なので、play
メソッドは、下記 のプログラムの 8 行目 のように、ゲーム盤の画像 を 描画した後 で、return 文 を 実行 して play
メソッドの 処理を終了 します。ここまでの処理 は 問題 は ありません。
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
中略
2 # ゲームの決着がついていない間繰り返す
3 while self.status == Marubatsu.PLAYING:
4 # ゲーム盤の表示
5 if verbose:
6 if gui:
7 self.draw_board(ax)
8 return
9 else:
10 print(self)
以下略
下図 は、gui
が True
の場合に、人間 VS AI の 対戦 が行われる際の、play
メソッドの フローチャート です。赤線の処理 が行われ、play
メソッドの 処理が終了 します。
その後 で、ゲーム盤 の マスの上 で マウスを押す と、下記 の on_mouse_down
が 実行 されて 着手を行う処理 が 実行 されます。
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)
問題 は、上記 の on_mouse_down
で 人間 が 着手を行う処理 を 行った後 で、AI が 着手を行う処理 がどこにも 記述されていない 点です。また、on_mouse_down
の 処理 を 行った後 で、ゲーム盤 の マスの上 で マウスを押す と、再び on_mouse_down
が 実行される ため、AI の手番 であるにも 関わらず、人間 が 着手を行う ことが できてしまいます。
そのことは、下記 の on_mouse_down
の フローチャート からも 確認 できます。
この問題 は、on_mouse_down
が 着手の処理 を 行った後 で、「次の手番 を AI が担当 する場合は その AI が 着手を選択 する」という 処理を記述 することで 解決 することが できます。どのようにその処理を記述すれば良いかについて少し考えてみて下さい。
問題を修正したフローチャート
下図 は、上記の処理を行 うように 修正 した on_mouse_down
の フローチャート です。
2024/04/14 修正:フローチャート に 画像を描画する処理 が 入っていませんでした が、入れたほうが良いと思いましたので、入れるよう に 修正 しました。
このように修正 することで、人間 が ゲーム盤 の 画像の上 で マウス を 押すたび に on_mouse_down
が 呼び出され、「人間の着手を行う」と「AI の着手を行う」の 処理 が 続けて行われる ようになるため、人間 VS AI の 対戦 が 行われる ようになります。
問題の修正
それぞれの手番 を 人間 と AI の どちらが担当するか を表す 情報 は、play
メソッドの 仮引数 ai
に 代入 されています。on_mouse_down
は play
メソッドの ローカル関数 なので、その ブロックの中 で ai
を そのまま利用 することが できます。従って、下記 のプログラムのように on_mouse_down
を 修正 することで、上記の 問題を解決 できます。
-
13 行目:マウスによる 着手 が 行われた後 の 手番 の
ai
の インデックス を 計算 する - 14 行目:AI の 手番であるか どうかを 判定 する
- 15 ~ 17 行目:AI の着手 を 計算 し、着手を行い、ゲーム盤の画像 を 更新 する
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 # 現在の手番を表す ai のインデックスを計算する
12 index = 0 if self.turn == Marubatsu.CIRCLE else 1
13 # ai が着手を行うかどうかを判定する
14 if ai[index] is not None:
15 x, y = ai[index](self, **params[index])
16 self.move(x, y)
17 self.draw_board(ax)
元と同じなので省略
18
19 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)
# 〇×ゲームを再起動する
self.restart()
# 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 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)
# ゲームの決着がついていない間繰り返す
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 = 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 ai[index] is not None:
+ x, y = ai[index](self, **params[index])
+ self.move(x, y)
+ self.draw_board(ax)
元と同じなので省略
Marubatsu.play = play
マウス による 着手の選択 と、AI による 着手の選択 の処理は、下記の表 のように 対応している ので 見比べて みて下さい。
処理 | 人間 | AI |
---|---|---|
着手を行う座標を計算する | 6、7 行目 | 15 行目 |
着手を行う | 8 行目 | 16 行目 |
画像を更新する | 9 行目 | 17 行目 |
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、人間が着手 を行うと 自動的 に AI が 着手を行う ようになります。下記 の実行結果は、(0, 0) の マスの上 で マウスを押した 場合の図で、AI が (2, 0) のマスに 着手を行う ことが 確認 できます。
mb.play(ai=[None, ai2], gui=True)
実行結果(実行結果はランダムなので下記とは異なる場合があります)
うまく プログラムが 動作している ように 見えるかもしれません が、実は、上記 のプログラムには 二つの問題 があります。実際 に AI と 何度か対戦 を 行ってみて、どのような 問題 があるかを 探してみて 下さい。
二つの問題の検証
一つ目の問題
一つ目の問題 は、〇 の着手 によって 勝利した後 で、AI が 着手を行ってしまう というものです。下記 は、〇 が勝利 する 着手を行った場合 の 図 です。〇 が勝利 しているにも 関わらず、× を担当 する AI が 着手 を 行っています。実際 に 〇 が勝利 する 着手を行った場合 に AI が 着手を行ってしまう ことを 確認 してみて下さい。
二つ目の問題
二つ目の問題 は、9 手目 の 着手 を 行った場合 に、下記 のような エラーが発生 してしまうというものです。実際 に 9 手目 の 着手 を 行った際 に、下記 の エラーが発生 することを 確認 してみて下さい。
略
File c:\Users\ys\ai\marubatsu\071\ai.py:235, in ai2(mb)
224 """ランダムなマスに着手する AI.
225
226 Args:
(...)
231 着手する座標を表す tuple
232 """
234 legal_moves = mb.calc_legal_moves()
--> 235 return choice(legal_moves)
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\random.py:373, in Random.choice(self, seq)
370 # As an accommodation for NumPy, we don't use "if not seq"
371 # because bool(numpy.array()) raises a ValueError.
372 if not len(seq):
--> 373 raise IndexError('Cannot choose from an empty sequence')
374 return seq[self._randbelow(len(seq))]
IndexError: Cannot choose from an empty sequence
Python の バージョン 3.10 では、下記 のような エラーメッセージ が 表示される ので、バージョン によって、同じエラー でも 表示 される エラーメッセージ が 若干変わる場合 が あるようです。ただし、エラーメッセージ が 若干が変わっても、この後で説明する、エラーメッセージ の 読み方 は 変わりません。
略
File c:\Users\ys\ai\marubatsu\071\ai.py:235, in ai2(mb)
224 """ランダムなマスに着手する AI.
225
226 Args:
(...)
231 着手する座標を表す tuple
232 """
234 legal_moves = mb.calc_legal_moves()
--> 235 return choice(legal_moves)
File c:\Users\ys\Anaconda3\envs\ml\lib\random.py:346, in Random.choice(self, seq)
344 """Choose a random element from a non-empty sequence."""
345 # raises IndexError if seq is empty
--> 346 return seq[self._randbelow(len(seq))]
IndexError: list index out of range
エラーメッセージの表記の意味
先に、エラーメッセージ という 手がかり がある、二つ目の問題 から 検証 することにします。これまで は、エラーメッセージ の中で、最後 に 表示された内容 を見て エラーの原因 を 推測 してきましたが、上記 の 「空(empty)の シーケンス型(sequence)から(from)選択(choose)することは できない(cannot)」という意味の IndexError: Cannot choose from an empty sequence
というエラーメッセージ だけから、エラーの原因 を 推測 することは 困難 です。また、その上に表示される、エラーが発生した行を表す、--> 378 return seq[self._randbelow(len(seq))]
は、自分で記述 した 覚えのない プログラムであり、これを見ても エラーの原因 を 推測 することは 困難 です。
このような場合 は、それより前 の エラーメッセージ を さかのぼって 原因を 推測 する 必要 が あります。そのためには、エラーメッセージ に どのような情報 が 表示 されているかを 理解する必要 が あります。
関数 や メソッド は、プログラム の 様々な場所 から 呼び出され て 利用 されます。そのため、関数の中 で エラーが発生 した場合は、その関数 が どこから呼び出されたか の情報を 知ること が、エラー が 発生するまで の プログラム の 処理の流れ を 辿るため に 重要 になります。そのため、エラーメッセージ には、エラー が 発生するまで に 呼び出された関数 が 順番に記述 されるようになっています。
エラーメッセージの読み方の具体例
言葉の説明だけではわかりづらいので、具体例 を 挙げます。下記 のプログラムは、2 つ の 関数 x
と y
を 定義 し、y()
の 返り値 を 表示 するプログラムです。
1 def x():
2 return a
3
4 def y():
5 return x()
6
7 print(y())
行番号のないプログラム
def x():
return a
def y():
return x()
print(y())
上記 の プログラム の 処理の流れ は以下のようになります。
-
7 行目 で
y()
を 実行 して 関数y
を 呼び出す -
4 行目 の 関数
y
が 呼び出される -
5 行目 で
x()
を 実行 して 関数x
を 呼び出す -
1 行目 の 関数
x
が 呼び出される -
2 行目 で
a
を 関数x
の 返り値 として 返す -
5 行目 で 関数
x
の 返り値 を、関数y
の 返り値 として 返す -
7 行目 で 関数
y
の 返り値 をprint
で 表示 する
ただし、実際 には、2 行目 の a
は 関数 x
の ブロックの中 で 一度 も 値が代入されていない ので、2 行目 を 実行 すると、下記 の 実行結果 の 最後の行 のように、a
が 定義されていない という 意味 の NameError: name 'a' is not defined
という エラーが発生 します。
実行結果
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[11], line 7
4 def y():
5 return x()
----> 7 print(y())
Cell In[11], line 5
4 def y():
----> 5 return x()
Cell In[11], line 2
1 def x():
----> 2 return a
NameError: name 'a' is not defined
上記 の エラーメッセージ の中の、最初の行 に 表示 される Traceback (most recent call last)
は、最も最近(most recent)に 呼び出された(call)ものを 最後(last)に 表示 するという 順番 で、プログラムの 処理 を さかのぼって(Traceback)表示 するという 意味 です。実際に、エラーメッセージ では、エラー が 発生するまで に 実行 された 処理の流れ が、関数 が 呼び出された順 で 表示 されています。
エラーメッセージ の表示の中の、Cell In [11], line 7
のような 表示 と、---->
が 表示された行 は、以下 のような 意味 を持ちます。
- エラー が 発生するまで に 実行 した 関数呼び出し が 記述 されている 行の情報を 表す。ただし、最後に表示 されたものは、エラー が 発生した行 の 情報 を表す
-
Cell In [11], line 7
のような 表記 は、関数呼び出し が行われた(または エラーが発生 した)JupyterLab の セルの番号 と、そのセルの中 の 行数 を表す -
---->
が 記述 されている 行 が、関数呼び出し が行われた(または エラーが発生 した)行の内容 を表す
上記 の エラーメッセージの右 に、上記の 処理の手順 の 対応を記述 したものを 下記 に 記します。エラーメッセージ が、処理の手順 の 順番通り に エラーが発生するまで の 処理の流れ を 表記 していることを 確認 して下さい。
Cell In[11], line 7
4 def y():
5 return x()
----> 7 print(y()) 1. y() を実行して 関数 y を呼び出す
Cell In[11], line 5
4 def y(): 2. 関数 y が呼び出される
----> 5 return x() 3. x() を実行して 関数 x を呼び出す
Cell In[11], line 2
1 def x(): 4. 関数 x が呼び出される
----> 2 return a 5. a を 関数 x の返り値として返す
NameError: name 'a' is not defined 上記の手順 5 でこのエラーが発生した
エラーメッセージ を 読み解く際 には、下から見る のが 一般的 です。その理由 は、下から見たほう が エラーの原因 を 見つけやすい場合 が 多い からです。例えば、上記 の エラーメッセージ の場合は、NameError: name 'a' is not defined
と その上部 に記述された x
の定義 の部分を 見るだけ で、a
が 定義されていない ことが 原因 であることが わかります。
二つ目のエラーメッセージの検証
下記 は、先程の 二つ目 の エラーメッセージ を 再掲 したものです。上記の例 は、JupyterLab の セルに記述 された プログラム でしたが、play
メソッドの 中 では、インポート した モジュール の ファイル に 記述 された プログラムが実行 される場合があります。そのような場合 は、Cell
ではなく、File
で始まる行 で、下記 のような 方法 で 関数呼び出し(または エラー)の 情報 が 記述 されます。
File ファイルのパス:行数, in その下のプログラムが記述された関数名(仮引数)
略
File c:\Users\ys\ai\marubatsu\071\ai.py:235, in ai2(mb)
224 """ランダムなマスに着手する AI.
225
226 Args:
(...)
231 着手する座標を表す tuple
232 """
234 legal_moves = mb.calc_legal_moves()
--> 235 return choice(legal_moves)
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\random.py:373, in Random.choice(self, seq)
370 # As an accommodation for NumPy, we don't use "if not seq"
371 # because bool(numpy.array()) raises a ValueError.
372 if not len(seq):
--> 373 raise IndexError('Cannot choose from an empty sequence')
374 return seq[self._randbelow(len(seq))]
IndexError: Cannot choose from an empty sequence
この エラーメッセージ を 下から見る と、下記 のようなことが わかります。
-
エラー が 発生した行 の 内容 は、
raise IndexError('Cannot choose from an empty sequence')
である -
File:(略)\random.py:373
から、エラー が 発生した行 は、random.py という ファイル の 373 行目 に 記述 されている -
in Random.choice(self, seq)
から、373 行目 は、Random
という クラス のchoice
という メソッドで定義 されており、この メソッド には、seq
という 仮引数 が 存在 する
373 行目 の raise
は、その 後ろに記述 した エラー を 発生する処理 を行います。実際 に raise
の 後ろ に 記述 されている 内容 が、最後の行 の エラーメッセージ と 対応 していることを 確認 して下さい。
raise
の使い方に関しては、必要になった時点で紹介する予定です。
上記 から random モジュールの choice
メソッドで エラーが発生 したことが わかります。
また、Cannot choose from an empty sequence
という エラーメッセージ から、おそらく choice
の 実引数 に 空のシーケンス型 の一つである 空の list を 記述 して 呼び出した ことが 原因 で エラーが発生 したことが 推測されます。
そのこと を 確認 するために、下記 のプログラムのように、random モジュールの choice
の 実引数 に、空の list を 記述 して 実行 すると、確かに 同じエラー が 発生 します。
from random import choice
print(choice([]))
実行結果
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[12], line 3
1 from random import choice
----> 3 print(choice([]))
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\random.py:373, in Random.choice(self, seq)
370 # As an accommodation for NumPy, we don't use "if not seq"
371 # because bool(numpy.array()) raises a ValueError.
372 if not len(seq):
--> 373 raise IndexError('Cannot choose from an empty sequence')
374 return seq[self._randbelow(len(seq))]
IndexError: Cannot choose from an empty sequence
choice
の 実引数 に 空の list を 記述 して 実行 したことが 原因 であることが わかりました が、何故 そのような処理 が 行われたか についてまでは わかりません。そこで、下記 の、先程 の エラーメッセージ の 一つ上に表示 された メッセージ を 検証 することにします。
略
File c:\Users\ys\ai\marubatsu\071\ai.py:235, in ai2(mb)
224 """ランダムなマスに着手する AI.
225
226 Args:
(...)
231 着手する座標を表す tuple
232 """
234 legal_moves = mb.calc_legal_moves()
--> 235 return choice(legal_moves)
上記 の エラーメッセージ から、下記 のようなことが わかります。
-
確かに、
return choice(legal_moves)
によって、choice
に 実引数legal_moves
を 記述 して 呼び出している -
File:(略)\ai.py:235
から、choice
を 呼び出した行 は、ai.py
という ファイル の 235 行目 に 記述 されている -
in ai2(mb)
から、この行 は、ai2
という 関数で定義 されており、この関数 には、mb
という 仮引数 が 存在 する
上記 から choice
は、ai.py
に 記述 された ai2
から 呼び出された ことが わかります。
ai.py
は、これまでに定義 した AI を 記述 した モジュール で、ai2
は、先程 play
メソッドを 実行した際 に、× の手番 を 担当 するように 指定 しました。
ai2
は、自分で定義 した 関数 なので、その中で 行う処理 は わかるはず です。忘れた方 は、以前の記事を 復習 してください。下記 は ai2
の定義 です。ai2
は、ランダムな着手 を行う AI で、ai2
が 行う処理 は、合法手 の 中から、random モジュール の choice
を使って ランダム に 選択を行う というものです1。
def ai2(mb):
legal_moves = mb.calc_legal_moves()
return choice(legal_moves)
今回の エラーが発生 した 状況 は、9 手目 の 着手 を 行った場合 です。上記 の エラーメッセージ から、ai2
が 呼び出されている ことがわかるので、9 手目 の 着手 を 行った後 で、ai2
が呼び出されて 着手を選択 する 処理を行った ことがわかります。
〇×ゲーム の ゲーム盤 の マスの数 は 9 マス なので、9 手目 の 着手 を 行った後 は、すべてのマス が 埋まっている ため、合法手 は 存在しません。従って、ai2
の 2 行目 の legal_moves = mb.calc_legal_moves()
を 実行 すると、legal_moves
には 空の list が 代入 され、3 行目 の return choice(legal_moves)
は、空の list を 実引数に記述 して choice
を 呼び出す ことになります。これで、エラー が 発生した原因 が 判明 しました。
このようなことがおきるのは、9 手目 の 着手後 に、AI が 着手を行ってしまう ことが 原因 です。従って、9 手目 の 着手後 に、AI が 着手 を 行わないようにする ことで、この 問題 を 解決できる ことが わかりました。
上記のように、エラーメッセージ を 読み解く ことで、エラーの原因 が 判明する場合 が あります。もちろん、エラーメッセージ を 読み解くだけ で すべて の エラーの原因 が わかる とは 限りません が、エラーメッセージ には、エラーの原因 の 重要なヒント が 記述 されている 場合が多い ので、エラーメッセージ の 意味を理解 することは デバッグ の 能力の向上 に つながる ため 非常に重要 です。
先程 の エラーメッセージ の 最初 には、下記 のような メッセージが表示 されます。ファイルのパス の中に ipympl
が 記述 されていることと、メッセージ(イベントのこと)を 処理 する(handle)という 意味 の _handle_message
という 名前 の 関数 で 処理が行われている ことから、イベントループ から、ipympl の仕組みを使ってで登録した イベントハンドラ の 処理 が 呼び出された ことが 推測 できます。
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\site-packages\ipympl\backend_nbagg.py:279,
in Canvas._handle_message(self, object, content, buffers)
一つ目と二つ目の問題の整理と統合
下記は、上記の検証 で 判明 した 一つ目 と 二つ目 の 問題点 です。
原因 | |
---|---|
一つ目の問題 | 〇 の勝利後 に AI が着手 する |
二つ目の問題 | 9 手目の着手後 に AI が着手 する |
上記 から、いずれの場合 も、特定の状況 の 後 で、AI が 着手してしまう ことが 問題 であることが わかります。従って、この 2 つ の それぞれの状況 に対して、AI が 着手を行わないようにする ことで、問題 を 解決 することが できます が、実は 上記の 二つの状況 を、一つの状況 に まとめる ことで、より簡単 に 問題 を 解決する ことが できます 。それが何かについて少し考えてみて下さい。
上記 の 2 つの問題 の 状況 に 共通する のは、ゲーム が 決着した状況 であるということです。また、深く考えなくても 下記のことはすぐにわかると思います。
- ゲーム が 決着していない 状況では、AI は 着手 を 行う必要 がある
- ゲーム が 決着した 状況では、AI は 着手 を 行ってはいけない
従って、ゲーム が 決着した 状況で、AI が 着手 を 行わないようにする ことで、二つの問題 を 同時に解決 することが できる ことが わかります。
問題の解決とフローチャート
問題の原因 は、on_mouse_down
が 行う処理 が、人間 が 着手 を 行った後 で、ゲーム の 決着がついているか どうかに 関係なく、AI が 着手を選択 する 点 なので、下記 のプログラムの 14 行目 のように、ゲーム の 決着がついていない場合のみ、AI が 着手を行うよう に 修正 することで、問題 を 解決 することが できます。
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 # 現在の手番を表す ai のインデックスを計算する
12 index = 0 if self.turn == Marubatsu.CIRCLE else 1
13 # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
14 if self.status == Marubatsu.PLAYING and ai[index] is not None:
15 x, y = ai[index](self, **params[index])
16 self.move(x, y)
17 self.draw_board(ax)
元と同じなので省略
18
19 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)
# 〇×ゲームを再起動する
self.restart()
# 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)
# ゲームの決着がついていない間繰り返す
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 = 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 ai[index] is not None:
+ 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)
元と同じなので省略
Marubatsu.play = play
下図 は、修正後 の on_mouse_down
の フローチャート です。
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、下記 の 2 つ の 実行結果 のように、人間の着手 で ゲーム の 決着がついた 場合に、AI が 着手 を 行わなくなる ことが 確認 できます。
mb.play(ai=[None, ai2], gui=True)
実行結果(実行結果はランダムなので下記とは異なる場合があります)
下図の後 で、× は 着手しません。
下図の後 で、エラー は 発生ません。
AI VS 人間 の対戦
先程と同様 に、play
メソッドで 実引数 に、gui=True
を 記述 して 下記 のプログラムのように、AI VS 人間 の 対戦 を 行う ことにします。下記 は、ai2
VS 人間 の 対戦 です。
mb.play(ai=[ai2, None], gui=True)
実行結果 は 示しません が、問題なく AI VS 人間 の 対戦が行える ことが 確認 できます。実際 に 対戦を行って、問題がない ことを 確認 してみて下さい。
AI VS 人間の対戦で行われる処理
対戦 を 問題なく行う ことが できる理由 は、play
メソッドを 実行 した際に、下記 のような 処理 が 行われる からです。
-
〇 を AI が担当 するので、下記 の
play
メソッドの 9 行目 でai[index]
はNone
ではない ため、10 行目 の return 文 は 実行されない - そのため、15 行目 が 実行され て AI が 着手を選択 する
-
次の繰り返し 処理では、× を 人間が担当 するので、下記 の
play
メソッドの 9 行目 でai[index]
はNone
になる ため、10 行目 の return 文 が 実行 されてplay
メソッドの 処理が終了 する - その後 は、ゲーム盤の上 で マウスを押す ことで「人間の着手」と「AI の着手」の 処理 が、人間 VS AI と 同様の手順 で 行われる
略
1 # ゲームの決着がついていない間繰り返す
2 while self.status == Marubatsu.PLAYING:
3 # 現在の手番を表す ai のインデックスを計算する
4 index = 0 if self.turn == Marubatsu.CIRCLE else 1
5 # ゲーム盤の表示
6 if verbose:
7 if gui:
8 self.draw_board(ax)
9 if ai[index] is None:
10 return
11 else:
12 print(self)
13 # ai が着手を行うかどうかを判定する
14 if ai[index] is not None:
15 x, y = ai[index](self, **params[index])
略
AI VS 人間の対戦のフローチャート
下図 は、gui
に True
が 代入 されていた場合に、play
メソッドで AI VS 人間 の 対戦 を 行った場合 の フローチャート です。赤い線 が、1 回目 の 繰り返し で行われる 処理 を、緑の線 が、2 回目 の 繰り返し で行われる 処理 を表します。
図から、1 手目 の 〇 の手番 は AI が 着手を行い、2 手目 の 着手 を 行う前 に play
メソッドが 終了 することが わかります。以後 は、人間 VS AI の場合と 同様 に、ゲーム盤の上 で マウスを押す と、on_mouse_down
が 呼び出され、人間の着手 と、AI の着手 が 続けて行われます。そのため、上記 のプログラムで AI VS 人間 の 対戦 を行うことが できます。
これで、GUI による AI との対戦 の 実装が完了 しました。最後に 念のため、上記の修正 を行っても 人間 VS 人間 と AI VS AI の 対戦 が 問題なく行えること を 確認 してみて下さい。
今回の記事のまとめ
今回の記事では、GUI で AI との対戦 が 行える ようにしました。
また、その際に、エラーメッセージ の 読み方 について 説明 しました。エラーメッセージ は デバッグ を 行う際 に 重要な手掛かり となるので、正しい読み方 を 理解する ことを 強くお勧めします。
次回の記事 では、GUI で ゲームのリセット などを 行える ようにします。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事
更新日時 | 更新内容 |
---|---|
2024/04/14 | フローチャートに画像を描画する処理を表記するように修正しました |