目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
図形によるマークの描画
前回の記事では、文字列 による マークの描画 の方法と、欠点 について 説明 しました。
今回の記事では、図形 を使って 〇 や × を 描画 するという 方法 を紹介します。この方法は、文字列を描画する方法と比べて、下記 のような 利点 が あります。
- マス の 真ん中 に 正確に描画 することが 簡単 に できる
- 枠の太さ を 自由に調整 することが できる
- 画像の大きさ を 変更 しても、図形 の 大きさ や 位置 を 調整 する 必要 が ない
なお、図形の大きさ を 試行錯誤 で 調整 する 必要 があるという 点 は、文字列 でも 図形 でも 変わりません。
図形による 〇 の描画
まず、(0, 0) のマスに 円 の 図形 を 描画 するプログラムを 記述 することにします。前回の記事で説明したように、円 の 図形の描画 は、円 の 図形 表す Artist を patches モジュールの Circle
メソッドで 作成 し、Axes の add_artist
メソッドで 登録 することで行います。その際に、円 の 中心の座標 と、半径 を 指定 する 必要 が あります。それらをどのように設定すればよいかについて少し考えてみて下さい。
下記は、(0, 0) のマスに 〇 を 配置 した ゲーム盤 の 画像 の 一例 です。
図 から、(0, 0) の マス の 中央 に 〇 を描画 するためには、円 の 中心の座標 を (0.5, 0.5)A1 に すれば良い ことが わかります。また、そのように設定 することで、円 を 必ず (0, 0) のマスの 中央 に 描画 できることが 保証 されます。この点は、試行錯誤 で 〇 を 描画する位置 を 探す必要 がある、文字列 による 〇 の 描画 に対する 大きな利点 です。
また、図 から、描画する 円の半径 を 0.5 以下 にする 必要 がある事も わかります。円の半径 については 試行錯誤 で 決める必要 があるので、とりあえず、下記のプログラムのように、0.3 を 設定 して 描画してみる ことにします。
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import japanize_matplotlib
mb = Marubatsu()
ax = mb.draw_board()
circle = patches.Circle([0.5, 0.5], 0.3)
ax.add_artist(circle)
実行結果
実行結果 から、円 が 塗りつぶされて おり、枠 も 描画されていない ことが わかります。また、円の大きさ が 若干小さい 気がするので、下記 のように 修正 することにします。
-
塗りつぶさない ようにするために、
fill=False
を 記述 する - 円 の 半径 を 0.35 に 増やす
なお、枠 と 塗りつぶし の 設定方法 について忘れた方は、以前の記事を参照して下さい。
ax = mb.draw_board()
circle = patches.Circle([0.5, 0.5], 0.35, fill=False)
ax.add_artist(circle)
修正箇所
ax = mb.draw_board()
-circle = patches.Circle([0.5, 0.5], 0.3)
+circle = patches.Circle([0.5, 0.5], 0.35, fill=False)
ax.add_artist(circle)
実行結果
実行結果 から、枠線 が 細い 気がするので、lw=2
を 記述 して 太く してみます。
ax = mb.draw_board()
circle = patches.Circle([0.5, 0.5], 0.35, fill=False, lw=2)
ax.add_artist(circle)
修正箇所
ax = mb.draw_board()
-circle = patches.Circle([0.5, 0.5], 0.35, fill=False)
+circle = patches.Circle([0.5, 0.5], 0.35, fill=False, lw=2)
ax.add_artist(circle)
実行結果
上記 の 実行結果 で 特に問題はない と思いますので、本記事 では、上記の設定 で 〇 を 描画 することにします。この設定が気に入らない人は、自由に変更して下さい。
また、〇 を 図形で描画 した場合は、文字列で描画 した場合と 異なり、下記 のプログラムのように、画像の大きさ を 変えて も 〇 が マスの真ん中 に 描画される という、マスの中 の 配置 が 変わらない という 利点 が 得られます。
ax = mb.draw_board(size=5)
circle = patches.Circle([0.5, 0.5], 0.35, fill=False, lw=2)
ax.add_artist(circle)
修正箇所
-ax = mb.draw_board()
+ax = mb.draw_board(size=5)
circle = patches.Circle([0.5, 0.5], 0.35, fill=False, lw=2)
ax.add_artist(circle)
実行結果
任意のマスに 〇 を描画する方法
上記 では、(0, 0) のマスに 〇 を描画 しましたが、実際 には、任意のマス に 〇 を描画 する 必要 が あります。いずれのマス に 〇 を描画 する場合でも、そのマス の 中心の座標 がわかれば、マスの 真ん中 に 〇 を描画 することが できます。
(x, y) の マス の 中心の座標 は、上図を見れば (x + 0.5, y + 0.5)A であることが簡単にわかるので、例えば (2, 1) のマスに 〇 を描画 するプログラムは 下記 のようになります。
x = 2
y = 1
ax = mb.draw_board()
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, fill=False, lw=2)
ax.add_artist(circle)
修正箇所
+x = 2
+y = 1
ax = mb.draw_board()
-circle = patches.Circle([0.5, 0.5], 0.35, fill=False, lw=2)
+circle = patches.Circle([x + 0.5, y + 0.5], 0.35, fill=False, lw=2)
ax.add_artist(circle)
実行結果
実行結果 から、(2, 1) のマスの 真ん中 に 〇 を 描画できている ことが 確認 できます。
draw_circle
の定義
(x, y) のマスに 〇 を描画 する たび に、上記 のようなプログラムを 毎回記述 するのは 面倒 なので、任意のマス に 〇 を描画 する、下記 のような メソッドを定義 する事にします。
-
名前:〇(circle)を 描画(draw)するので、
draw_circle
という 名前 にする - 処理:(x, y) のマスに 〇 を描画 する
-
入力:
x
とy
という 仮引数 に、〇 を描画 する マスの座標 を 代入 する。また、描画 を 行うため には Axes が 必要になる ので、ax
という 仮引数 に Axes を 代入 する - 出力:なし
下記は、draw_circle
の 定義 を行うプログラムです。
def draw_circle(self, ax, x, y):
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, fill=False, lw=2)
ax.add_artist(circle)
Marubatsu.draw_circle = draw_circle
下記は、draw_circle
を使って、すべてのマス に 〇 を描画 するプログラムです。実行結果 から 正しく動作 することが 確認 できます。
- 1 行目:ゲーム盤の枠 を 描画 する
- 2 ~ 4 行目:2 重 の for 文 を使って、ゲーム盤 の すべてのマス に対して、4 行目 で 〇 を描画 する
ax = mb.draw_board()
for x in range(3):
for y in range(3):
mb.draw_circle(ax, x, y)
実行結果
図形による × の描画
次に、(0, 0) のマスに × を 図形で描画 するプログラムを 記述 することにします。× の 図形 は 2 本 の 線 によって 構成 されるので、plot
メソッドで 描画 することが できます。2 本 の 線 の 両端の座標 をどのように設定すればよいかについて少し考えてみて下さい。
下記は、(0, 0) のマスに × を配置 した ゲーム盤 の 画像 の 一例 です。先程 〇 を 半径 が 0.35 の 円で描画 したので、× を 同じ大きさ で 描画 するために、それぞれの 線 の 両端 の x 座標 と y 座標 を、マスの中央 の (0.5, 0.5)A から 0.35 だけ 増減した値 に 設定 しました。
下記 は 、上図を元 に、(0, 0) のマスに × を描画 するプログラムです。c = "k"
によって 線の色 を 黒 に、lw=2
によって、線の太さ を 〇 の図形 と 同じ 2 に 設定 しました。
ax = mb.draw_board()
ax.plot([0.15, 0.85], [0.15, 0.85], c="k", lw="2")
ax.plot([0.15, 0.85], [0.85, 0.15], c="k", lw="2")
実行結果
実行結果 から、うまく描画 できていることが 確認 できます。
draw_cross
の定義
draw_circle
と 同様 の 考え方 で、任意のマス に × を描画 する draw_cross
メソッドを、下記 のプログラムのように 定義 することが できます。
def draw_cross(self, ax, x, y):
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c="k", lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c="k", lw="2")
Marubatsu.draw_cross = draw_cross
下記 は、draw_cross
を使って、すべてのマス に × を描画 するプログラムです。実行結果 から 正しく動作 することが 確認 できます。
ax = mb.draw_board()
for x in range(3):
for y in range(3):
mb.draw_cross(ax, x, y)
実行結果
ゲーム盤に配置された 〇 と × の描画
任意のマス に 〇 と × を 描画できる ようになったので、draw_board
を ゲーム盤 に 配置 された マークの描画 を 行うよう に 修正 します。具体的には、下記 のプログラムのように、2 重の for 文 を使って すべてのマス に対して、〇 が配置 されていれば draw_circle
を、× が配置 されていれば draw_cross
を呼び出して マークを描画 するプログラムを 追加 します。
- 3 ~ 8 行目:2 重 の for 文 を使って、ゲーム盤 の すべてのマス に対して処理を行う
-
5、6 行目:(x, y) に 〇 が配置 されていれば
draw_circle
で 〇 を描画 する -
7、8 行目:(x, y) に × が配置 されていれば
draw_cross
で × を描画 する
1 def draw_board(self, size=3):
元と同じなので省略
2 # ゲーム盤のマークを描画する
3 for y in range(self.BOARD_SIZE):
4 for x in range(self.BOARD_SIZE):
5 if self.board[x][y] == Marubatsu.CIRCLE:
6 self.draw_circle(ax, x, y)
7 elif self.board[x][y] == Marubatsu.CROSS:
8 self.draw_cross(ax, x, y)
9
10 # ax を返り値として返す
12 return ax
13
14 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size])
# y 軸を反転させる
ax.invert_yaxis()
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
if self.board[x][y] == Marubatsu.CIRCLE:
self.draw_circle(ax, x, y)
elif self.board[x][y] == Marubatsu.CROSS:
self.draw_cross(ax, x, y)
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
# ゲーム盤のマークを描画する
+ for y in range(self.BOARD_SIZE):
+ for x in range(self.BOARD_SIZE):
+ if self.board[x][y] == Marubatsu.CIRCLE:
+ self.draw_circle(ax, x, y)
+ elif self.board[x][y] == Marubatsu.CROSS:
+ self.draw_cross(ax, x, y)
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
下記 は、いくつか の マス に 着手 を 行った後 で draw_board
で ゲーム盤を描画 するプログラムです。実行結果 から、正しく動作 していることが 確認 できます。
mb.move(0, 0)
mb.move(1, 1)
mb.move(2, 1)
mb.draw_board()
実行結果
直前に着手したマークの表示色の変更
ゲーム盤 を 文字列で描画 する場合は、下記 の 実行結果 のように、マーク を 大文字で表示 することで、直前に着手 した マーク を 示して いました。
print(mb)
実行結果
Turn x
o..
.xO
...
画像 で ゲーム盤 を 描画 する場合は、大文字 と 小文字 で 区別 することは できない ので、別の方法 で 直前に着手 した マーク を 示す必要 があります。どのような方法で示せばよいかについて、少し考えてみて下さい。
ぱっと思いつく方法として、下記 のような 方法 があるでしょう。このうち、マークの色 を 変える 方法が 最も簡単 なので、本記事 では その方法を採用 することにします。マス の 塗りつぶしの色 を 変える 方法や、他に もっと良い方法 を思いついた人は、時間と興味があれば実際に実装してみて下さい。
- マークの色 を 変える
- マス の 塗りつぶしの色 を 変える
マークの色 を 変える ためには、マーク を 描画する処理 を行う draw_circle
メソッドと、draw_cross
メソッドに、マークの色 を 代入 する 仮引数を追加 する 必要 が あります。本記事では その仮引数 を 下記 のように 設定 することにします。
-
仮引数 の 名前 を
color
とする -
デフォルト値 に
"black"
を 設定 した、デフォルト引数 とする
下記は、color
を 追加 した draw_circle
と draw_cross
の 定義 を行うプログラムです。
-
1、7 行目:デフォルト値 を
"black"
とする、デフォルト引数color
を 追加 する -
2 行目:
ec=color
を記述して、円の枠 をcolor
で 指定した色 で 描画 する -
8、9 行目:
c=color
を記述して、線 をcolor
で 指定した色 で 描画 する
1 def draw_circle(self, ax, x, y, color="black"):
2 circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
3 ax.add_artist(circle)
4
5 Marubatsu.draw_circle = draw_circle
6
7 def draw_cross(self, ax, x, y, color="black"):
8 ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
9 ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
10
11 Marubatsu.draw_cross = draw_cross
行番号のないプログラム
def draw_circle(self, ax, x, y, color="black"):
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
Marubatsu.draw_circle = draw_circle
def draw_cross(self, ax, x, y, color="black"):
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
Marubatsu.draw_cross = draw_cross
修正箇所
-def draw_circle(self, ax, x, y):
+def draw_circle(self, ax, x, y, color="black"):
- circle = patches.Circle([x + 0.5, y + 0.5], 0.35, fill=False, lw=2)
+ circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
Marubatsu.draw_circle = draw_circle
-def draw_cross(self, ax, x, y):
+def draw_cross(self, ax, x, y, color="black"):
- ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], lw="2")
+ ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
- ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], lw="2")
+ ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
Marubatsu.draw_cross = draw_cross
plot
メソッドで 線の色 を 指定 する際の キーワード引数 は、c
と color
の どちら を使っても かまいません が、Circle
メソッド の キーワード引数 に c
を 使う ことはで きません(エラーが発生 します)。おそらく、枠線の色 を表す ec
と 塗りつぶしの色 を表す fc
との 区別がつきづらい からではないかと思います。
次に、draw_board
を 下記 のように 修正 します。なお、下記 のプログラムでは、直前に着手 した マークの色 を 赤色 に設定しました。他の色が良い人は自由に変更して下さい。
-
5 行目:変数
color
に、(x, y) が 直前に着手 を行った マスと同じ 場合は、"red"
を、そうでなければ"black"
を 代入 する -
7、9 行目:キーワード引数
color=color
を 記述 することで、color
に代入 された 色 で マークを描画 する
1 def draw_board(self, size=3):
元と同じなので省略
2 # ゲーム盤のマークを描画する
3 for y in range(self.BOARD_SIZE):
4 for x in range(self.BOARD_SIZE):
5 color = "red" if (x, y) == self.last_move else "black"
6 if self.board[x][y] == Marubatsu.CIRCLE:
7 self.draw_circle(ax, x, y, color=color)
8 elif self.board[x][y] == Marubatsu.CROSS:
9 self.draw_cross(ax, x, y, color=color)
10
11 # ax を返り値として返す
12 return ax
13
14 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size])
# y 軸を反転させる
ax.invert_yaxis()
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
if self.board[x][y] == Marubatsu.CIRCLE:
self.draw_circle(ax, x, y, color=color)
elif self.board[x][y] == Marubatsu.CROSS:
self.draw_cross(ax, x, y, color=color)
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
+ color = "red" if (x, y) == self.last_move else "black"
if self.board[x][y] == Marubatsu.CIRCLE:
- self.draw_circle(ax, x, y)
+ self.draw_circle(ax, x, y, color=color)
elif self.board[x][y] == Marubatsu.CROSS:
- self.draw_cross(ax, x, y)
+ self.draw_cross(ax, x, y, color=color)
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
下記 のプログラムのように、draw_board
を実行すると、直前に着手 した (2, 1) のマスの 〇 が 赤く表示 されることが 確認 できます。
mb.draw_board()
実行結果
draw_circle
と draw_cross
の統合
上記 のプログラムでも 問題なくゲーム盤 を 描画できます が、同じような処理 を行う、draw_circle
と draw_cross
を、1 つ のメソッドに まとめる ことで、プログラム を 少し簡潔 にすることが できます。
具体的 には、この 2 つの関数 を draw_mark
という メソッド に まとめます。その際に、draw_mark
に、どのマーク を 描画するか を 代入 する 仮引数 を 追加 する 必要 が あります。そこで、下記のプログラムのように 仮引数 mark
を 追加 することにします。
-
1 行目:メソッド の 名前 を
draw_mark
に 修正 し、仮引数mark
を 追加 する -
2 ~ 4 行目:
mark
がMarubatsu.CIRCLE
の場合は、〇 を描画 する処理を 記述 する -
5 ~ 7 行目:
mark
がMarubatsu.CROSS
の場合は、× を描画 する処理を 記述 する
1 def draw_mark(self, ax, x, y, mark, color="black"):
2 if mark == Marubatsu.CIRCLE:
3 circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
4 ax.add_artist(circle)
5 elif mark == Marubatsu.CROSS:
6 ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
7 ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
8
9 Marubatsu.draw_mark = draw_mark
行番号のないプログラム
def draw_mark(self, ax, x, y, mark, color="black"):
if mark == Marubatsu.CIRCLE:
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
elif mark == Marubatsu.CROSS:
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
Marubatsu.draw_mark = draw_mark
draw_mark
を 定義 する事で、draw_board
を、下記 のように 少し簡潔 に 記述できます。
6 行目:draw_mark
の 実引数 に、(x, y) の マスのマーク を表す self.board[x][y]
を 記述 することで、(x, y) の マークを描画 する
1 def draw_board(self, size=3):
元と同じなので省略
2 # ゲーム盤のマークを描画する
3 for y in range(self.BOARD_SIZE):
4 for x in range(self.BOARD_SIZE):
5 color = "red" if (x, y) == self.last_move else "black"
6 self.draw_mark(ax, x, y, self.board[x][y], color)
7
8 # ax を返り値として返す
9 return ax
10
11 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size])
# y 軸を反転させる
ax.invert_yaxis()
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
- if self.board[x][y] == Marubatsu.CIRCLE:
- self.draw_circle(ax, x, y, color=color)
- elif self.board[x][y] == Marubatsu.CROSS:
- self.draw_cross(ax, x, y, color=color)
+ self.draw_mark(ax, x, y, self.board[x][y], color)
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
なお、(x, y) の マス が 空の場合 に、draw_mark
を 呼び出す のは おかしい と 思うかもしれません が、その場合 は 以下の処理 が 行われる ので 問題はありません。
-
draw_mark
の 仮引数mark
にMarubatsu.EMPTY
が 代入 される -
draw_mark
の if 文 の 2 つの条件式 は、いずれもFalse
になる - 従って、
draw_mark
は何の 処理 も 行わない
下記 は、修正後 の draw_board
を使って、ゲーム盤を描画 するプログラムです。実行結果 から 正しく動作 することが 確認 できます。
mb.draw_board()
実行結果
なお、以後 は draw_circle
と draw_cross
はもう 使わない ので、marubatsu.py
には それらのメソッド は 記述しません。
静的メソッドとしての draw_mark
の定義
この部分 の 内容 は、初心者 には わかりづらい と思いますので、意味が分からない場合 は 現時点 では 読み飛ばしても構いません。また、本記事では、draw_mark
メソッドの 定義 の 直前の行 に @classmethod
を 記述 することにしますが、draw_mark
は 通常のメソッド と 同じ方法 で 呼び出して利用 することが できる ので、当面は、@classmethod
が記述されていても、その存在を 気にする必要 は ありません。
下記 の draw_mark
の 定義 の ブロックの中 では、self
は 一度 も 使われていません。
def draw_mark(self, ax, x, y, mark, color="black"):
if mark == Marubatsu.CIRCLE:
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
elif mark == Marubatsu.CROSS:
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
Marubatsu.draw_mark = draw_mark
このようなメソッド は、仮引数 から self
を削除 することで、下記のプログラムのように、通常の関数 として 定義 する事が できます。
def draw_mark(ax, x, y, mark, color="black"):
if mark == Marubatsu.CIRCLE:
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
elif mark == Marubatsu.CROSS:
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
修正箇所
-def draw_mark(self, ax, x, y, mark, color="black"):
+def draw_mark(ax, x, y, mark, color="black"):
if mark == Marubatsu.CIRCLE:
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
elif mark == Marubatsu.CROSS:
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
-Marubatsu.draw_mark = draw_mark
しかし、draw_mark
は、Marubatsu
クラス の中 で 利用 することを 想定 して 定義 したものなので、通常の関数 として 定義 すると、そのこと が わかりづらく なってしまいます。
このような場合は、以前の記事で 簡単に説明 した、静的メソッド として draw_mark
を 定義 するという 方法 があります。静的メソッド は、下記 のプログラムのように、メソッドの定義 の 直前の行 に、@staticmethod
という デコレータ を記述し、仮引数 から self
を削除 することで 定義 します。
@staticmethod
def draw_mark(ax, x, y, mark, color="black"):
if mark == Marubatsu.CIRCLE:
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
elif mark == Marubatsu.CROSS:
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
Marubatsu.draw_mark = draw_mark
修正箇所
+@staticmethod
-def draw_mark(self, ax, x, y, mark, color="black"):
+def draw_mark(ax, x, y, mark, color="black"):
if mark == Marubatsu.CIRCLE:
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
elif mark == Marubatsu.CROSS:
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
Marubatsu.draw_mark = draw_mark
静的メソッド として draw_mark
を 定義 しても、Marubatsu
クラスの インスタンスから、draw_mark
を 呼び出す ことが できるの で、draw_mark
を 利用 する draw_board
の プログラム を 修正 する 必要 は ありません。実際に、draw_board
を修正しなくても、下記 のプログラムのように、draw_board
を使って ゲーム盤 を 描画 することが できます。
mb.draw_board()
実行結果
上記の性質から、わざわざ draw_mark
を 静的メソッド で 定義 する 意味がない と思う人がいるかもしれませんので、静的メソッド の メリット をいくつか紹介します。
- その クラス に 密接 に 関係がある関数 であることが 明確 になる
- インスタンス に 関する処理 が 記述されていない ことが 明確 になる
- インスタンス を 作成しなくても、クラス から 直接呼び出し て 利用できる
最後の利点 の 具体例 を紹介します。下記 は、〇×ゲーム を リセットした後 で、2 行目 で ゲーム盤を描画 し、その後 で draw_mark
メソッドで (0, 0) と (1, 1) に 〇 と × を 描画 するプログラムです。3 行目 では Marubatsu
クラスの インスタンス から draw_mark
を 呼び出して いますが、4 行目 では Marubatsu
クラスから 直接 draw_mark
を 呼び出して います。
mb.restart()
ax = mb.draw_board()
mb.draw_mark(ax, 0, 0, Marubatsu.CIRCLE)
Marubatsu.draw_mark(ax, 1, 1, Marubatsu.CROSS)
実行結果
draw_mark
を 静的メソッド として 定義していない 場合は、Marubatsu
クラス から 直接 draw_mark
を 呼び出す と エラーが発生 します。例えば、今回の記事 で 定義 した draw_circle
メソッドは 静的メソッドではない ので、下記のプログラムのように、Marubatsu
クラスから 直接呼び出そうとする と、エラーが発生 します。
Marubatsu.draw_circle(ax, 2, 2)
実行結果
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[28], line 1
----> 1 Marubatsu.draw_circle(ax, 2, 2)
TypeError: draw_circle() missing 1 required positional argument: 'y'
上記のエラーメッセージは、以下のような意味を持ちます。
-
TypeError
データ型(Type)に関するエラー -
draw_circle() missing 1 required positional argument: 'y'
draw_circle
を呼び出す際に必要とされる(required)、位置引数 (positional argument)y
に対応する実引数(argument)が 1 つ存在しない(missing)
上記のエラー は、以前の記事で説明したように、クラスのメソッド を クラスから呼び出した 場合 は、通常の関数 と 同じ処理 が行われるため、draw_circle
メソッドの 仮引数 self
に、対応 する 実引数 が 記述されていない ことが 原因 で 発生 します。
初心者 には上記の 利点 が わかりづらい ので、静的メソッド は、無理に使う必要 は ありません が、利用 すると 便利な場合がある ので 紹介しました。他にも 本記事 では 紹介していない使い道 があるので、興味がある方は調べてみると良いでしょう。
手番と結果の描画
ゲーム盤 を 文字列で表示 する場合は、下記 の実行結果のように、ゲーム盤 の 上 に、手番 や 勝敗の結果 を 表示 していました。そこで、 ゲーム盤 を 画像で描画 する場合も、ゲーム盤 の 上 に その情報 を 描画 することにします。なお、先程 restart
メソッドで、ゲーム盤をリセット したので、下記 のプログラムでは、いくつかのマス に 着手 を行っています。
mb.move(0, 0)
mb.move(1, 1)
mb.move(2, 1)
print(mb)
実行結果
Turn x
o..
.xO
...
手番 や 勝敗の結果 の 情報 は、文字列の情報 なので、Axes の text
メソッドを 利用 する 必要 が あります。下記 は、文字列 で ゲーム盤 を 表示 する プログラムの中 で、上部 に 表示 する メッセージを作成 する 部分 で、この部分 のプログラムを 参考 することにします。
def __str__(self):
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status + "\n"
以下略
ゲーム盤 の 左上の座標 は (0, 0)A なので、とりあえずその位置 に 文字列を描画 することにします。下記 は、draw_board
に 上記 のプログラムと 同じメッセージ を 描画 する 処理を追加 したプログラムです。なお、元のプログラム では、この メッセージの後 に、ゲーム盤 を表す 文字列 を 表示 するので、text
の 最後 に 改行 を表す "\n"
を 結合 していましたが、画像 で 文字列を描画 する場合は、この メッセージの後 に 文字列 は 存在しない ので 削除 しました。
-
4 ~ 8 行目:画面上部 に 描画 する 文字列 を 作成 し、
text
に 代入 する -
9 行目:
text
を (0, 0)A の 位置 に 描画 する
1 def draw_board(self, size=3):
元と同じなので省略
2 # 上部のメッセージを描画する
3 # ゲームの決着がついていない場合は、手番を表示する
4 if self.status == Marubatsu.PLAYING:
5 text = "Turn " + self.turn
6 # 決着がついていれば勝者を表示する
7 else:
8 text = "winner " + self.status
9 ax.text(0, 0, text)
10
11 # ゲーム盤の枠を描画する
元と同じなので省略
12
13 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size])
# y 軸を反転させる
ax.invert_yaxis()
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status
ax.text(0, 0, text)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
+ if self.status == Marubatsu.PLAYING:
+ text = "Turn " + self.turn
+ # 決着がついていれば勝者を表示する
+ else:
+ text = "winner " + self.status
+ ax.text(0, 0, text)
# ゲーム盤の枠を描画する
元と同じなので省略
Marubatsu.draw_board = draw_board
下記 は、修正後 に draw_board
を実行して ゲーム盤を表示 するプログラムです。
mb.draw_board()
実行結果
実行結果 から、以下の問題 がある事が わかります。
- 文字列 が 小さすぎる
- 文字列 の 位置 が、ゲーム盤 と くっついている ように見える
- 枠 と 目盛り が 邪魔
そこで、下記 のプログラムのように、文字列 を 大きく し、位置 を 少し上に調整 し、枠 と 目盛り の 表示を消す ように、draw_board
を修正 します。
- 3 行目:枠 と 目盛り を 表示しない ようにする
-
12 行目:文字列 の 描画位置 を 0.2 だけ 上にずらし、
fontsize
を20
に 設定 して 文字列 を 大きくする
1 def draw_board(self, size=3):
元と同じなので省略
2 # 枠と目盛りを表示しないようにする
3 ax.axis("off")
4
5 # 上部のメッセージを描画する
6 # ゲームの決着がついていない場合は、手番を表示する
7 if self.status == Marubatsu.PLAYING:
8 text = "Turn " + self.turn
9 # 決着がついていれば勝者を表示する
10 else:
11 text = "winner " + self.status
12 ax.text(0, -0.2, text, fontsize=20)
元と同じなので省略
13
14 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size])
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
# 枠と目盛りを表示しないようにする
+ ax.axis("off")
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status
+ ax.text(0, -0.2, text, fontsize=20)
元と同じなので省略
Marubatsu.draw_board = draw_board
下記 は、修正後 に draw_board
を 実行 して ゲーム盤を描画 するプログラムです。実行結果 から、問題はなさそう であることが 確認 できました。文字列の表示内容、大きさ、表示位置をさらに調整したい人は、自由に変更して下さい。
mb.draw_board()
実行結果
下記 は、ゲーム の 決着がついた場合 に draw_board
を 実行 して ゲーム盤を描画 するプログラムです。実行結果 から、こちらも 問題はなさそう であることが 確認 できました。
mb.restart()
mb.move(0, 0)
mb.move(0, 1)
mb.move(1, 0)
mb.move(1, 1)
mb.move(2, 0)
mb.draw_board()
実行結果
play
メソッドの修正
ゲーム盤 を 画像で描画できる ようになったので、最後に play
メソッドで、ゲーム盤 を 画像で表示できる ように、play
メソッドを 下記 のように 修正 することにします。
-
ゲーム盤 を 文字列 と 画像 の どちらで表示 するかを 代入 する 仮引数
gui
を 追加 する -
gui
にFalse
が 代入 されている場合は 文字列 で、True
が 代入 されている場合は、画像 で ゲーム盤を表示 する -
これまで の
play
メソッドとの 互換性を保つ ために、gui
を デフォルト値 がFalse
の デフォルト引数 とする
下記は、上記 のように play
メソッドを 修正 したプログラムです。
-
1 行目:
False
を デフォルト値 とする デフォルト引数gui
を 追加 する -
4 ~ 8、10 ~ 14 行目:ゲーム盤を表示 する際に、
gui
がTrue
の場合は、draw_board
メソッドで 描画 するように 修正 する
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
元と同じなので省略
2 while self.status == Marubatsu.PLAYING:
3 # ゲーム盤の表示
4 if verbose:
5 if gui:
6 self.draw_board()
7 else:
8 print(self)
元と同じなので省略
9 # 決着がついたので、ゲーム盤を表示する
10 if verbose:
11 if gui:
12 self.draw_board()
13 else:
14 print(self)
15
16 return self.status
17
18 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board()
else:
print(self)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# 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()
else:
print(self)
return self.status
Marubatsu.play = play
修正箇所
-def play(self, ai, params=[{}, {}], verbose=True, seed=None):
+def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
元と同じなので省略
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
if verbose:
- print(self)
+ if gui:
+ self.draw_board()
+ else:
+ print(self)
元と同じなので省略
# 決着がついたので、ゲーム盤を表示する
if verbose:
print(self)
+ if gui:
+ self.draw_board()
+ else:
+ print(self)
return self.status
Marubatsu.play = play
下記 は、gui=True
を 実引数に記述 して play
メソッドで ai2
どうし の 対戦 を行うプログラムです。実際 の 実行結果 は、画像 が 縦に並びます が、下記 では 横に並べます。
from ai import ai2
mb = Marubatsu()
mb.play(ai=[ai2, ai2], gui=True)
実行結果(実行結果はランダムなので下記とは異なる場合があります)
念のため に、下記 のプログラムで gui
を 記述せず に play
メソッドを 実行 してみます。実行結果 から、これまでどおり、文字列 で ゲーム盤が表示 されることが 確認 できました。
from ai import ai2
mb = Marubatsu()
mb.play(ai=[ai2, ai2])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
Turn o
...
...
...
Turn x
...
...
.O.
Turn o
..X
...
.o.
Turn x
O.x
...
.o.
Turn o
oXx
...
.o.
Turn x
oxx
O..
.o.
Turn o
oxx
o..
.oX
winner o
oxx
o..
Oox
人間との対戦
次に、下記 のプログラムを実行して、ai2
と対戦 してみることにします。
mb.play(ai=[None, ai2], gui=True)
実行 すると わかる と思いますが、ゲーム盤の画像 が 描画されません。また、テキストボックスに exit を 入力 して ゲームを中断 すると、その時点 で ゲーム盤の画像 が下記のように 描画 されます。このようなことが起きる理由について少し考えてみて下さい。
exit を 入力後 の実行結果
上記のようなことがおきる 理由 は、以前の記事で説明したように、画像の描画 が plt.show()
が 実行 された 時点 で 行われる からです。play
メソッドが 行う処理の中 では、plt.show()
は 実行されない ため、play
メソッドが 終了するまで の間で、ゲーム盤の画像 は 描画されません。
なお、exit を 入力した後 で、ゲーム盤の画像 が 描画される のは、JupyterLab の セルの実行 が 終了 して 自動的 に plt.show()
が 呼び出される ためです。
細かい話 ですが、AI どうし の 対戦後 に、複数 の ゲーム盤の画像 が まとめて描画 されるのは、下記 のような 処理 が行われているからです。
-
draw_board
の中で、subplots
メソッドが 実行されるたび に、新しい Figure が 作成 されて pyplot に登録される -
draw_board
は、新しく作成 した Figure に ゲーム盤の画像 の 描画を行う - 従って、AI どうし の 対戦 が行われた場合は、
draw_board
が 呼ばれた数 だけ、Figure が作成 され、pyplot に 登録 される -
plt.show()
が 実行 された際に、複数 の Figure が登録 されていた場合は、登録 されている すべての Figure の 画像 が 順番に描画 される
play
メソッドで 人間が対戦 する場合は、ゲーム が 終了するまで に、plt.show()
が 実行されない ため、ゲーム盤 が 表示されません。この問題 は、下記 のプログラムの 3 行目 のように、draw_board
の中で、すべて の 描画の処理 を 行った後 で、plt.show()
を実行するように 修正 することで 解決 することができます。なお、plt.show()
を 実行 すると、pyplot に 登録 された Figure は 自動的に削除される ので、ax
に代入された Axes も 利用できなくなります。そのため、ax
を return 文 で 返す処理 は 削除 しました。
1 def draw_board(self, size=3):
元と同じなので省略
2 # ゲーム盤を描画する
3 plt.show()
4
5 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size])
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
# ゲーム盤を描画する
plt.show()
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
# ゲーム盤を描画する
+ plt.show()
# ax を返り値として返す
return ax
Marubatsu.draw_board = draw_board
実行結果は示しませんが、下記 のプログラムを実行して ai2
と対戦 すると、今度は ゲーム盤の画面 が 表示 されるようになることが 確認 できます。
mb.play(ai=[None, ai2], gui=True)
今回の記事のまとめ
今回の記事 では、〇×ゲーム の ゲーム盤 を 画像で描画 し、実際 に play
メソッド で ゲーム盤の画像 を 表示 して 遊ぶことができるよう に しました。これで GUI の 出力 を行う部分は 完成 しましたが、座標の入力 は、相変わらず 文字で入力 を行う 必要 が あります。
次回の記事では、GUI の 入力部分 の 処理 を 記述する方法 を 紹介 します。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事