1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その67 画像によるマークの描画

Last updated at Posted at 2024-03-28

目次と前回の記事

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

図形によるマークの描画

前回の記事では、文字列 による マークの描画 の方法と、欠点 について 説明 しました。

今回の記事では、図形 を使って ×描画 するという 方法 を紹介します。この方法は、文字列を描画する方法と比べて、下記 のような 利点あります

  • マス真ん中正確に描画 することが 簡単できる
  • 枠の太さ自由に調整 することが できる
  • 画像の大きさ変更 しても、図形大きさ位置調整 する 必要ない

なお、図形の大きさ試行錯誤調整 する 必要 があるという は、文字列 でも 図形 でも 変わりません

図形による 〇 の描画

まず、(0, 0) のマスに 図形描画 するプログラムを 記述 することにします。前回の記事で説明したように、図形の描画 は、図形 表す Artistpatches モジュールの Circle メソッドで 作成 し、Axesadd_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) のマスに 〇 を描画 する
  • 入力xy という 仮引数 に、〇 を描画 する マスの座標代入 する。また、描画行うため には 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_circledraw_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 メソッドで 線の色指定 する際の キーワード引数 は、ccolorどちら を使っても かまいません が、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_circledraw_cross の統合

上記 のプログラムでも 問題なくゲーム盤描画できます が、同じような処理 を行う、draw_circledraw_cross を、1 つ のメソッドに まとめる ことで、プログラム少し簡潔 にすることが できます

具体的 には、この 2 つの関数draw_mark という メソッドまとめます。その際に、draw_mark に、どのマーク描画するか代入 する 仮引数追加 する 必要あります。そこで、下記のプログラムのように 仮引数 mark追加 することにします。

  • 1 行目メソッド名前draw_mark修正 し、仮引数 mark追加 する
  • 2 ~ 4 行目markMarubatsu.CIRCLE の場合は、〇 を描画 する処理を 記述 する
  • 5 ~ 7 行目markMarubatsu.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仮引数 markMarubatsu.EMPTY代入 される
  • draw_markif 文2 つの条件式 は、いずれも False になる
  • 従って、draw_mark は何の 処理行わない

下記 は、修正後draw_board を使って、ゲーム盤を描画 するプログラムです。実行結果 から 正しく動作 することが 確認 できます。

mb.draw_board()

実行結果

なお、以後draw_circledraw_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
...

手番勝敗の結果情報 は、文字列の情報 なので、Axestext メソッドを 利用 する 必要あります下記 は、文字列ゲーム盤表示 する プログラムの中 で、上部表示 する メッセージを作成 する 部分 で、この部分 のプログラムを 参考 することにします。

    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 だけ 上にずらしfontsize20設定 して 文字列大きくする
 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追加 する
  • guiFalse代入 されている場合は 文字列 で、True代入 されている場合は、画像ゲーム盤を表示 する
  • これまでplay メソッドとの 互換性を保つ ために、guiデフォルト値Falseデフォルト引数 とする

下記は、上記 のように play メソッドを 修正 したプログラムです。

  • 1 行目Falseデフォルト値 とする デフォルト引数 gui追加 する
  • 4 ~ 8、10 ~ 14 行目ゲーム盤を表示 する際に、guiTrue の場合は、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利用できなくなります。そのため、axreturn 文返す処理削除 しました。

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 です。

次回の記事

  1. 前回の記事で説明したように、本記事では、matplotlibAxes座標ゲーム盤のマスの座標区別 するために、右下小さな A表記 します

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?