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?

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その66 枠線と文字列によるマークの描画

1
Last updated at Posted at 2024-03-24

目次と前回の記事

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

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

ルールベースの AI の一覧

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

Artist を登録するメソッドに関する今後の説明の方法

最初に、Artist を登録 する メソッド に関する 今後 の 説明の方法 を説明します。

前回の記事で、matplotlib は plt.show() を 実行するまで は、画像 を 描画しない と説明しました。例えば plot メソッドは、実際 に Axes に 線 や 折れ線 を表す 図形 の Artist を 登録するだけ で、画像の描画 は 行いません。

ただし、plot などの 説明 をする際に、「Axes に Artist を登録 する」や、「実際の描画 は plt.show() で行う」のような 説明 を行うと、まわりくどい、意味が分かづらい 説明になってしまいます。そのため、以後 は plot メソッド など、Artist を 登録 する 処理の説明 を、「〇〇を描画する」 のように 説明する ことにします。

2 種類の座標の表記の区別

ゲーム盤 の マスの座標 と、matplotlib の Axes が管理する 座標 は、異なる座標 です。この 2 つの座標 を 混同 すると、間違った処理 が 行われる点 に 注意 して下さい。

前回の記事 では、どちらの座標 も (0, 0) のように 記述 してきましたが、どちらの座標を表すか が 区別しづらい ので、以後は ゲーム盤のマスの座標 は、これまでどおり (0, 0) のように 表記 し、Axes の座標 は (0, 0)A のように、右下 に 小さな A を 表記 することにします。

ただし、プログラム の コメントの中 での 表記 は 区別しません。

ゲーム盤の描画

今回の記事では、前回の記事で説明した matplotlib の機能を使って、〇×ゲーム の ゲーム盤 を 画像 で 描画 します。まず、Marubatsu クラスに、ゲーム盤 を 描画 するための 下記 のような メソッド を 定義 する事にします。

  • 名前:ゲーム盤(board)を 描画(draw)するので、draw_board という 名前 にする
  • 処理:ゲーム盤 の 画像 を 描画 する
  • 入力:なし
  • 出力:なし

下記は、draw_board メソッドの 定義 です。なお、メソッドの処理はこれから記述します。

from marubatsu import Marubatsu

def draw_board(self):
    pass

Marubatsu.draw_board = draw_board

ゲーム盤の描画で行う処理の考察

画像 で ゲーム盤 を 表示 する 処理 を 記述 する際に、何をすれば良いかがわからない場合は、これまでの 文字列 での ゲーム盤の表示 を行う 処理 を 参考 にすると良いでしょう。

下記は、文字列 で ゲーム盤の表示 を行う プログラム です。

mb = Marubatsu()
mb.move(0, 0)
mb.move(1, 1)
print(mb)

実行結果

Turn o
o..
.X.
...

上記の 表示内容 から、ゲーム盤を描画 するためには、下記 の内容を 描画 する 必要がある ことが わかります。

  • 手番 や ゲームの結果 の 情報
  • ゲーム盤 に 配置 された マーク

また、文字列 での ゲーム盤 には 表示していません が、画像 で ゲーム盤を描画 する際には、ゲーム盤の マスを区別 するための、枠線の描画 が 必要 になります。

そこで、それら を 順番 に 実装する ことにします。

ゲーム盤の枠線の描画

ゲーム開始時 の 〇×ゲーム の ゲーム盤 を 画像で描画 する場合、おそらくほとんどの方は、下記 のような 画像 を 思い浮かべる のではないかと思います。

この画像は、4 本の線1で 構成 されているので、Axes の plot メソッドを 利用 して ゲーム盤 の 枠線 を 描画 することが できます。

前回の記事では、plot メソッドで 折れ線グラフ を 描画 する方法を 紹介 しましたが、plot メソッドの 実引数 に 記述 する、x 座標 と y 座標 の それぞれ の 座標の数 を 2 つ にすることで、線 を 描画 することが できます。

そこで、4 つの線 の 両端 の 座標 を どのように設定 するかについて 少し考えてみて 下さい。ピンとこない人 は、ノート や グラフ用紙 などに 上記の図 を 実際 に 書いてみて、それぞれの線の両端の 点の座標 が どうなるか を 考えてみる と 良い でしょう。

枠線の両端の座標の設定例

下記 は、線 の 両端の座標 の 設定例 です。ゲーム盤 の 左上の座標 を 原点(0, 0)A とし、縦方向 の 枠線 は、原点 から x 軸方向 に 1 ずつ 離れた位置 に 描画 します。横方向 の 枠線 も 同様 です。なお、ゲーム盤 の 枠 が 上図 のように 配置 されるのであれば、下図以外 の 設定 でも 構いません。興味と時間がある方は、他の設定でゲーム盤を描画してみて下さい。

このような 図 をノートなどに 書く ことで、枠線 の 両端の座標 が 理解しやすく なります。プログラムで 図形 などを 組み合わせ て 画像を描画 する際に、わからなくなった場合 は、図 を 書いてみる ことを 強くお勧めします。

上図 は、matplotlib で 描画 したものです。詳細は長くなるので省略しますが、矢印 による 注釈 は、Axes の annotate というメソッドを使って 描画 することができます。例えば、(0, 0)A の 矢印 と 注釈 は、下記 のプログラムのように 記述 します。

ax.annotate('(0, 0)', xy=(0, 0), xytext=(0.35, 0.6),
            arrowprops=dict(facecolor='black', shrink=0.05))

annotate メソッドの詳細については、下記のリンク先を参照して下さい。

枠線の描画を行うプログラム

下記 は、上図 を 参考 に、それぞれの 枠線 の 両端の座標 を 列挙 したものです。

  • (0, 1)A と (3, 1)A
  • (0, 2)A と (3, 2)A
  • (1, 0)A と (1, 3)A
  • (2, 0)A と (2, 3)A

下記 のプログラムは、plot メソッドに 上記の座標 を 記述 して、4 つの線 を 描画 するプログラムです。plot メソッドは 一度 で 1 つの線(または折れ線)しか 描画 できないので、下記のように、それぞれ の 枠線ごと に、plot メソッドを 呼び出す必要 が あります。

  • 4 行目:subplots メソッドで Figure と Axes を 作成 し、それぞれを 変数に代入 する
  • 5 ~ 8 行目:Axes の plot メソッドを呼び出して 4 つの線 を 描画 する
1  import matplotlib.pyplot as plt
2  import japanize_matplotlib
3
4  fig, ax = plt.subplots(figsize=[3, 3])
5  ax.plot([0, 3], [1, 1]) # (0, 1) と (3, 1) を結ぶ線
6  ax.plot([0, 3], [2, 2]) # (0, 2) と (3, 2) を結ぶ線
7  ax.plot([1, 1], [0, 3]) # (1, 0) と (1, 3) を結ぶ線
8  ax.plot([2, 2], [0, 3]) # (2, 0) と (2, 3) を結ぶ線
行番号のないプログラム
import matplotlib.pyplot as plt
import japanize_matplotlib

fig, ax = plt.subplots(figsize=[3, 3])
ax.plot([0, 3], [1, 1]) # (0, 1) と (3, 1) を結ぶ線
ax.plot([0, 3], [2, 2]) # (0, 2) と (3, 2) を結ぶ線
ax.plot([1, 1], [0, 3]) # (1, 0) と (1, 3) を結ぶ線
ax.plot([2, 2], [0, 3]) # (2, 0) と (2, 3) を結ぶ線

実行結果

plot メソッド は、線 や 折れ線 の 頂点 の それぞれの座標 を [0, 1] のように 記述 するの ではなく、それぞれの座標 を x 座標 と y 座標 に 分けて記述 する点に 注意 して下さい。よくある間違い ですが、下記 のように 記述 すると、(0, 1)A と (3, 1)A を結ぶ線 ではなく、(0, 3)A と (1, 1)A を 結ぶ線 が 描画 されてしまいます。

fig, ax = plt.subplots(figsize=[3, 3])
ax.plot([0, 1], [3, 1])

実行結果

上記 の 実行結果 には 示しませんでした が、上記 のプログラムを 実行 すると、最初 に 下記 のような 表示 が 行われます。

JupyterLab では、セルの最後 に 実行 された 処理の結果 が 表示される ようになっており、これは、最後の行 の plot メソッドが 返す Artist です。

本記事では、以降 も、このような表示 は 実行結果の表記 から 省略 します。

[<matplotlib.lines.Line2D at 0x29ffa9bc790>]

上記 の 実行結果 のように、plot で 線を描画 すると、勝手 に 色 が ついて しまいます。これは、plot が 折れ線グラフ を 描画するため のメソッドなので、複数 の 折れ線 を 描画 した際に、区別がつく ように 自動的 に 異なる色 で 表示 するという 機能を持つ からです。

線 を 黒色 で 描画 したい場合は、下記 のプログラムのように、c という キーワード引数 に、黒色 を表す "k" を記述します。わかりづらいと思った人は、color="black" を 記述 しても 構いません。下記 のプログラムでは、5 行目 で そのように記述 してみました。

fig, ax = plt.subplots(figsize=[3, 3])
ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線
ax.plot([1, 1], [0, 3], c="k") # (1, 0) と (1, 3) を結ぶ線
ax.plot([2, 2], [0, 3], color="black") # (2, 0) と (2, 3) を結ぶ線
修正箇所
fig, ax = plt.subplots(figsize=[3, 3])
-ax.plot([0, 3], [1, 1]) # (0, 1) と (3, 1) を結ぶ線
+ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
-ax.plot([0, 3], [2, 2]) # (0, 2) と (3, 2) を結ぶ線
+ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線
-ax.plot([1, 1], [0, 3], c="k") # (1, 0) と (1, 3) を結ぶ線
+ax.plot([1, 1], [0, 3]) # (1, 0) と (1, 3) を結ぶ線
-ax.plot([2, 2], [0, 3], color="black") # (2, 0) と (2, 3) を結ぶ線
+ax.plot([2, 2], [0, 3]) # (2, 0) と (2, 3) を結ぶ線

実行結果

実行結果 から、すべての枠線 が 黒色 で 描画 されることが 確認 できます。

Axes の 表示範囲に関する補足

plot メソッドなど、Axes の グラフを描画 するメソッドの 多く は、実行 することで、Axes の 表示範囲 を、描画 する すべての図形 がぴったり 収まる ような 範囲より も 少しだけ広い範囲 になるように 自動的 に 設定 してくれます。上図 の場合は x 座標 と y 座標 の 範囲 には -0.15 ~ 3.15 が 設定 されます。自動的 に 設定 された 表示範囲 に 問題がない 場合は、set_xlim 等のメソッドを 記述 する 必要 は ありません。

〇×ゲーム の ゲーム盤 を 描画 する際に、表示範囲 を ゲーム盤 の 大きさ に ぴったり合わせる ことも できます が、上記の画像 で特に 違和感はない ので そのままにする ことにします。ぴったり合わせたい方は set_xlim 等のメソッドを記述して下さい。

Axes の x 軸 と y 軸 の 表示範囲 は、下記のプログラムのように、get_xlim と get_ylim というメソッドで知ることができます。描画 される 画像 は 同じ なので 省略 しました。

fig, ax = plt.subplots(figsize=[3, 3])
ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線
ax.plot([1, 1], [0, 3], c="k") # (1, 0) と (1, 3) を結ぶ線
ax.plot([2, 2], [0, 3], color="black") # (2, 0) と (2, 3) を結ぶ線
print(ax.get_xlim())
print(ax.get_ylim())
修正箇所
fig, ax = plt.subplots(figsize=[3, 3])
ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線
ax.plot([1, 1], [0, 3], c="k") # (1, 0) と (1, 3) を結ぶ線
ax.plot([2, 2], [0, 3], color="black") # (2, 0) と (2, 3) を結ぶ線
+print(ax.get_xlim())
+print(ax.get_ylim())

実行結果

(-0.15000000000000002, 3.15)
(-0.15000000000000002, 3.15)

上記の 実行結果 で、-0.15000000000000002 のように 表示 されるのは、2 進数 で、10 進数 の -0.15 を 表現 すると、1.00110011・・・ のように、小数点以下 に 0011 が 無限に繰り返される ような 数値 になるためです。Python の float2 は 2 進数 で 数値を表現 するので、-0.15 を 正確 に 表現 することは できない ため、上記のような 微小な誤差 が 発生 します。

上記 では 3.15 は 正確 に 表現できている ように 見えるかもしれません が、実は 3.15 も 同様 に、2 進数 では 正確 に 表現できません。上記で 3.15 のように 表示 されるのは、四捨五入 した 結果 が、3.15 になった からです。

なお、Axes の 表示範囲 の場合は、このような 微小な誤差 は、画像の描画 に 影響を与えない ので、気にする必要 は ありません。

筆者のホームページに、2 進数 と 10 進数 の 変換を行うツール を作りましたので、興味がある方はリンク先を見て下さい。他にも、色や画像のデジタル表現の解説や文字コードの変換ツールなどもあります。

get_xlim と get_ylim の詳細については、下記のリンク先を参照して下さい。

軸の反転

上図は、先程の実行結果です。上図 で うまくいっている ように 見えるかもしれません が、実は上図には 大きな問題 が あります。それが何かを少し考えてみて下さい。

matplotlib は、グラフを描画 するために 利用 されることが 多い ので、y 座標 は 数学と同様 に、上方向 が 正の値 になります。一方、コンピュータ が扱う 画像の座標 は、一般的 に y 座標 は 下方向 が 正の値 になります。実際に、上図 をよく見ると、y 座標 が 上方向 が 正の値 になっていることがわかります。

コンピュータ の 画像 で、y 座標 が 下方向 が 正の値 になっているのは、コンピューターでは 一般的 に 上から下 に 文字など を 描画 するからだと 思います。

matplotlib では、下記 のプログラムのように、Axes の invert_yaxis メソッドを実行することで、y 軸(y axis)の 方向 を 逆にする(invert)ことが できます。〇×ゲーム では 利用しません が、x 軸 の 方向 を 逆 にする invert_xaxis もあります。

fig, ax = plt.subplots(figsize=[3, 3])
ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線
ax.plot([1, 1], [0, 3], c="k") # (1, 0) と (1, 3) を結ぶ線
ax.plot([2, 2], [0, 3], color="black") # (2, 0) と (2, 3) を結ぶ線
ax.invert_yaxis()
修正箇所
fig, ax = plt.subplots(figsize=[3, 3])
ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線
ax.plot([1, 1], [0, 3], c="k") # (1, 0) と (1, 3) を結ぶ線
ax.plot([2, 2], [0, 3], color="black") # (2, 0) と (2, 3) を結ぶ線
+ax.invert_yaxis()

実行結果

実行結果 から y 座標 の 上下が反転 していることが 確認 できます。

draw_board メソッドへの組み込み

上記 のプログラムを draw_board メソッドに 組み込む と 下記 のようになります。y 軸を反転 させる 処理 は、plt.show() で 画像 を 実際に描画 する 前 であれば、どこで行っても構わない ので、下記のプロラムでは、ゲーム盤の枠 を 描画 するよりも 前で行う ことにしてみました。ax.invert_yaxis() を 最後に記述したい 人は、 順番 を 入れ替えて 下さい。

matplotlib が、plt.show() を 実行するまで 画像を 描画しない理由 の 一つ は、Artist を 登録するたび に 画像 を 描画 すると、軸の方向 や、Axes の 表示範囲 を 変更 した際に、それまでに描画した 画像 を 描画し直す必要 が 生じる からです。

なお、〇×ゲーム の ゲーム盤を描画 する際に、Axes の 枠や目盛り の 描画 は 消したほうが良い のですが、この後 で ゲーム盤 に 〇 など を 描画 する際に、目盛り が 表示 されていた方が わかりやすい ので、しばらく は 目盛りを表示したまま にすることにします。

def draw_board(self):
    fig, ax = plt.subplots(figsize=[3, 3])

    # y 軸を反転させる
    ax.invert_yaxis()

    # ゲーム盤の枠を描画する
    ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
    ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線
    ax.plot([1, 1], [0, 3], c="k") # (1, 0) と (1, 3) を結ぶ線
    ax.plot([2, 2], [0, 3], color="black") # (2, 0) と (2, 3) を結ぶ線

Marubatsu.draw_board = draw_board

下記 のプログラムで、Marubatsu クラスの インスタンスを作成 し、draw_board を 実行 してみます。実行結果 から ゲーム盤 が 描画される ことが 確認 できました。

mb = Marubatsu()
mb.draw_board()

実行結果

draw_board メソッドの修正 その 1

上記 の draw_board メソッドでは、4 本 の 枠線 を 描画 するために、plot メソッドを 4 回 呼び出していますが、その処理 を 繰り返し処理 を使って 記述 することが できます。また、そのように修正 することで、例えば 5 x 5 など の、3 x 3 以外 の 大きさ の 〇×ゲーム の ゲーム盤 も 表示できる ようになるという 利点 が 得られます。

まず、下記 の 横線を描画 するプログラムを、繰り返し で 表示する方法 を考えて下さい。

    ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
    ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線

上記 のプログラムを 観察 すると 下記 の事が わかります。

  • x 座標 を表す [0, 3] は 共通 する
  • x 座標 のデータのうちの 3 は、ゲーム盤 の 大きさ を 表す 3 である
  • y 座標 を表すデータは、[1, 1]、[2, 2] のように 規則正しく増えて いる
  • y 座標 を表すデータは、1 から ゲーム盤 の 大きさ を表す 3 未満 まで 増えて いる

ゲーム盤のサイズ は、BOARD_SIZE 属性に 代入 されているので、上記 のプログラムは、下記 のように for 文 を使って 記述 することが できます。

    for i in range(1, self.BOARD_SIZE):
        ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線

また、縦線 を 描画 するプログラムも、同様 に、下記 のプログラムで 記述 できます。

    for i in range(1, self.BOARD_SIZE):
        ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線

上記 の 2 つのプログラムは、1 つにまとめる ことが できるの で、draw_board は 下記 のように 定義 することが できます。

def draw_board(self):
    fig, ax = plt.subplots(figsize=[3, 3])

    # 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") # 縦方向の枠線        

Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self):
    fig, ax = plt.subplots(figsize=[3, 3])

    # y 軸を反転させる
    ax.invert_yaxis()

    # ゲーム盤の枠を描画する
    # ゲーム盤の枠を描画する
-   ax.plot([0, 3], [1, 1], c="k") # (0, 1) と (3, 1) を結ぶ線
-   ax.plot([0, 3], [2, 2], c="k") # (0, 2) と (3, 2) を結ぶ線
-   ax.plot([1, 1], [0, 3], c="k") # (1, 0) と (1, 3) を結ぶ線
-   ax.plot([2, 2], [0, 3], color="black") # (2, 0) と (2, 3) を結ぶ線
+   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") # 縦方向の枠線        

Marubatsu.draw_board = draw_board

下記 のプログラムで、修正 した draw_board で ゲーム盤を描画 すると、実行結果 から、ゲーム盤 が 正しく描画 されることが 確認 できます。

mb.draw_board()

実行結果

また、下記 のプログラムで、ゲーム盤 の サイズ を 5 x 5 にした場合でも draw_board で ゲーム盤 を 正しく描画 されることが 確認 できます。

mb = Marubatsu(board_size=5)
mb.draw_board()

実行結果

draw_board メソッドの修正 その 2

上記の draw_board は、figsize が [3, 3] の サイズ の 画像を描画 しますが、ゲーム盤 の 画像の大きさ を 自由 に 調整できる ほうが 便利 です。そこで、draw_board の 仮引数 に、ゲーム盤 の 画像の大きさ を 代入 する size という 仮引数 を 追加 します。また、size に対して、以下 のような 工夫を行う ことにします。

  • 〇×ゲーム の ゲーム盤 は 正方形 なので、size に [3, 3] のような list を 記述 するの ではなく、一つ の 3 のような 数値を代入 するようにする
  • draw_board を 呼び出す 際に、毎回 size に 対応 する 実引数 を 記述 するのは 面倒 なので、3 を デフォルト値 とする デフォルト引数 にする

下記は、そのように draw_board を 修正 したプログラムです。

  • 1 行目:デフォルト値 に 3 を 設定 した デフォルト引数 size を 追加 する
  • 2 行目:figsize に [size, size] を 設定 して subplots を 呼び出す ように 修正 することで、描画 する 画像の大きさ を size で 設定 できる ようにする
def draw_board(self, size=3):
    fig, ax = plt.subplots(figsize=[size, size])
以下同じなので略
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") # 縦方向の枠線        

Marubatsu.draw_board = draw_board
修正箇所
-def draw_board(self):
+def draw_board(self, size=3):
-   fig, ax = plt.subplots(figsize=[3, 3])
+   fig, ax = plt.subplots(figsize=[size, size])
以下同じなので略
Marubatsu.draw_board = draw_board

下記 は、draw_board の 実引数 の記述で size を省略 した場合と、size に 5 を設定 した場合のプログラムです。前回の記事で説明したように、1 つ の JupyterLab のセル で、複数 の 画像を描画 する場合は、plt.show() を 実行 する 必要 が あります。実行結果 から、ゲーム盤 の 画像の大きさ を 変える ことが できるようになった とが 確認 できます。

なお、先程 mb に 5 x 5 の ゲーム盤を代入 したので、下記 では mb を 作り直し ています。

mb = Marubatsu()
mb.draw_board()
plt.show()
mb.draw_board(size=5)

実行結果



文字列によるマークの描画

次は、〇 と × の マーク を ゲーム盤に描画 する 処理 を 記述 します。

これまでと同様 に、〇 と × の 文字列 を 画像に描画 するという 方法 も考えられますが、文字列 で 〇 や × などを 描画 する 方法 には、下記 のような 問題 が あります。

  • 表示する 文字列 の 位置 を うまく調整 しないと、マスの真ん中 に 表示できない
  • 文字列 の 大きさ を 変える と、位置 の 調整 を やり直す必要 がある
  • 〇 や × の 線の太さ を 変更 することが 難しい

わかりづらいと思いますので、具体例 を挙げながら 説明 します。

まず、(0, 0) のマスに 〇 を 文字列で描画 するプログラムを 記述 することにします。前回の記事で説明したように、文字列 は Axes の text メソッドで 描画 することが できます が、その際に、描画する 文字列 の 左下の座標 を 指定 する 必要 が あります。その座標をどのように設定すればよいかについて少し考えてみて下さい。

座標をどのように設定すればよいかが良くわからない場合は、先程説明したように、ノート や グラフ用紙 などに、想定する画像 を 書いてみる ことを 強くお勧めします。下図 は、(0, 0) のマスに 〇 を配置 した ゲーム盤 の 画像 の 一例 です。

上図 から、この画像 を 描画 するためには、text メソッドで "〇" を (0, 1)A の 座標付近 に 描画 すればよいことが わかります。下記 は その処理 を行うプログラムです。

  • 3 行目:左下の座標 が (0, 1)A の 位置 に "〇" を 描画 する
1  def draw_board(self, size=3):
元と同じなので省略
2      # (0, 0) のマスに 〇 を描画する
3      ax.text(0, 1, "〇")
4  
5  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") # 縦方向の枠線   

    # (0, 0) のマスに 〇 を描画する
    ax.text(0, 1, "〇")

Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
    # (0, 0) のマスに 〇 を描画する
+   ax.text(0, 1, "〇")

Marubatsu.draw_board = draw_board

下記は、修正 した draw_board で ゲーム盤を描画 するプログラムです。

mb.draw_board()

実行結果

実行結果 からわかるように、〇 は確かに (0, 0) の マスの中 に 描画 されますが、文字 の 大きさ が 小さすぎ ます。文字 の 大きさ は、text メソッドの fontsize という キーワード引数 で 設定 できますが、ちょうどいい大きさ の 〇 を描画 するためには、さまざまな大きさ で 〇 を描画 し、自分の目 で 確かめる という、試行錯誤 を 行う必要 が あります。

〇 のフォントサイズの調整

〇 の フォントサイズ を 変更 する際に、毎回 draw_board メソッドを 定義し直す のは 大変 です。そこで、下記 のプログラムの 5 行目 のように、draw_board メソッドが、その内部の処理で 作成 した Axes が 代入 されている ax を 返り値 として 返す ように 修正 することで、draw_board メソッドを 実行した後 で、〇 を 描画できる ようにします。

  • 2 行目 の 下 に 記述 されていた (0, 0) に 〇 を描画 する 処理 を 削除 する
  • 5 行目:ax を 返り値 として 返す ように 修正 する
1  def draw_board(self, size=3):
元と同じなので省略
2      # ここにあった、(0, 0) のマスに 〇 を描画するプログラムを削除する
3
4      # ax を返り値として返す
5      return ax
6
7  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") # 縦方向の枠線   

    # ax を返り値として返す
    return ax

Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
    # (0, 0) のマスに 〇 を描画する
-   ax.text(0, 1, "〇")
    
    # ax を返り値として返す
+   return ax

Marubatsu.draw_board = draw_board

上記 のように draw_board を 修正 することで、下記 の プログラム のように、draw_board の 返り値 である Axes を使って、ゲーム盤の枠線 を 描画した後 で、引き続き 〇 を描画 するプログラムを 記述できる ようになります。実行結果は先程と同じなので省略します。

ax = mb.draw_board()
ax.text(0, 1, "〇")

下記 は、for 文 を使って、fontsize に 20、30、40、50、60 を 設定 して 〇 を描画 するプログラムです。なお、実際 の 実行結果 は、画像 が 縦に並んで描画 されますが、わかりやすさ を 重視 して、下記の 実行結果 では、画像 を 横に並べて表記 します。

for size in range(20, 70, 10):
    ax = mb.draw_board()
    ax.text(0, 1, "〇", fontsize=size)
    plt.show()

実行結果

実行結果 から、fontsize を 50 にすると ちょうど良さそう であることが わかります。なお、どれくらい の大きさが ちょうど良い かは、人によって 感じ方が 異なる ので、50 以外 の大きさが 良い と 思った人 は、自由に変更 しても かまいません。

〇 の描画位置の調整

上記 の fontsize を 50 に 設定 した場合の 図 から、〇 の 位置 をもう 少し上に移動 したほうが よさそう であることが わかります。大きさの調整 と 同様 に、ちょうどよい位置 は、試行錯誤 で 調べる必要 が あります。

下記 は、for 文 を使って、〇 の 表示位置 の y 座標 を 0.75、0.8、0.85、0.9、0.95 に 設定 して 〇 を描画 するプログラムです。なお、range は 整数しか扱えない3 ので、list を使って 繰り返し処理 を 行っています。

for y in [0.75, 0.8, 0.85, 0.9, 0.95]:
    ax = mb.draw_board()
    ax.text(0, y, "〇", fontsize=50)
    plt.show()

実行結果 から、y 座標 を 0.85 にすると ちょうど良さそう であることが わかります。この位置 が 気に入らない 方は、x 座標 を 含めて自由に調整 しても かまいません。

文字列による 〇 の描画の問題点

上記の手順 を見て、かなり面倒 だと 思った人 が 多いのではないか と思いますが、文字列 による 〇 の描画 には 他にも問題 があります。その一つは 文字列 の 描画 では 〇 の 枠の太さ を 柔軟に変更 することが できない という問題です。text メソッドは、fontweight という キーワード引数 を使って、文字列の太さ を 変更できる のですが、フォントの種類 によっては、設定 を 行っても 文字列の 太さが変わらない 場合が あるようです。実際 に、筆者のパソコン では、下記 のプログラムの 実行結果 のように、fontweight に 細字 を表す "light" と 太字 を表す "bold" を 設定 しても 文字列の太さ は 変わりませんでした。

plt.text(0, 0, "abc", fontsize=50, fontweight="light")
plt.text(0.3, 0, "abc", fontsize=50, fontweight="bold")

他の問題点 として、画像の大きさ を 変更 した場合に、Axes の 表示範囲 は 変化しない ので、図形 の 位置と大きさ はそれにあわせて 変化 しますが、表示される 文字列の大きさ は 変わらない というものが あります(文字列 の 位置 は 変化します)。

下記は、draw_board の実引数に size=5 を記述することで 大きなゲーム盤 を 描画した後 で、先程 と 同じ位置 と 大きさ で 〇 を描画 するプログラムです。

mb.draw_board(size=5)
plt.text(0, 0.85, "〇", fontsize=50)

実行結果

実行結果 からわかるように、画像のサイズ が 変化 したことで、枠線 も それに合わせた位置 と 大きさ で 描画 されています。一方、〇 の 文字列 の 描画 は 以下 のように 行われます。

  • 〇 の 左下 の 座標 の 位置 は、画像のサイズ の 変化 に 合わせた位置 で 表示 される
  • 〇 の 文字列 の 描画 の 大きさ は、画像のサイズ が 変化 しても 変わらない

従って、マスの中 での 〇 の 描画位置 が、真ん中 から 左下 に ずれて しまいます。この 問題を解決 するためには、再び 文字の大きさや位置を 試行錯誤で調整 する 必要 があるため 大変 です。なお、この問題 は、× を 文字列 で 描画 する際にも 発生 します。興味がある方は、実際に × を同じ方法で描画してみて下さい。

〇 や × のような 図形を描画 する際に、文字列 を使って 描画 を行うと、いくつかの問題 が 発生する ことが、実感 できたのではないでしょうか。

今回の記事のまとめ

今回の記事 では、〇×ゲーム の ゲーム盤 の 枠線 と、文字列 による マーク の 描画 の方法について説明しました。また、文字列 で マーク を描画する方法の 欠点 を説明しました。

次回の記事 では、図形 を使って マーク を 描画 する方法を 説明 し、ゲーム盤 を 描画 するプログラムを 完成 させます。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した marubatsu.py です。

次回の記事

  1. 数学 では、このような 両端がある線 のことを、線分 と呼びますが、一般的 には 線分 のことも 線と呼ぶ ことが 多い ので、本記事 では 線分 のことも 線と表記 します ↩

  2. 本記事では取り扱いませんが、Python には、10 進数 の 数値 を 正確に表現 するための、decimal という データ型 が あります ↩

  3. 整数以外 の値を 扱う ことが できる、range と 同様の機能 を持つ 関数 に、numpy というモジュールの arange という 関数 が あります が、説明 すると 長くなる ので、今回の記事 では list を利用 しました。numpy や arange については今後の記事で説明する予定です ↩

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?