0
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を一から作成する その201 さまざまなデータ型での手番とマスの表現

Posted at

目次と前回の記事

Python のバージョンとこれまでに作成したモジュール

本記事のプログラムは Python のバージョン 3.13 で実行しています。また、numpy のバージョンは 2.3.5 です。

以下のリンクから、これまでに作成したモジュールを見ることができます。本文で説明しますが、今回の下記のファイルは前回のファイルから修正を行っています

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
mbtest.py テストに関する関数
util.py ユーティリティ関数の定義
tree.py ゲーム木に関する Node、Mbtree クラスなどの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

整数型のデータでの手番とマスの表現

これまでのプログラムでは、手番とゲーム盤のマスのデータ を下記の表のように 文字列型のデータで表現 してきました。

手番とマスのデータ データ
〇 の手番とマーク "o"(小文字の o)
× の手番とマーク "x"(小文字の x)
空のマス "."(ピリオド)

また、Marubatsu クラスの status 属性に代入される ゲームの状態 としては下記の 文字列型 のデータで表現してきました。その際に 〇 の勝利× の勝利 を表すデータは、上記の 手番とマークと同じデータ を用いました。

手番とマスのデータ データ
ゲーム中 "playing"
〇 の勝利 "o"(小文字の o)
× の勝利 "x"(小文字の x)
引き分け "draw"

以下の説明では、これらのうち 手番、マーク、〇 または × の 勝利の状態 を表すデータを 手番とマスのデータと表記 することにします。

手番とマスのデータ数値型 などの、他のデータで表現 することもできます。それぞれに対してどのようなデータを割り当てるかは自由ですが、数値型 のデータを割り当てる場合は 何もないことを表すデータに整数の 0 を、それ以外のデータ に対しては 1 から順番に整数を割り当てる ことが多いので、下記の表の割り当てで ndarray でゲーム盤を表現する NpIntBoard クラスを定義 することにします。クラスの名前は numpy の整数型(integer)の ndarray でゲーム盤を表現することからそのように命名しました。

手番とマスのデータ データ
空のマス 0
〇 の手番とマーク 1
× の手番とマーク 2

ListBoard などの、他のゲーム盤を表すデータ型でも今回の記事でこれから説明するのと同様の方法で手番とマスのデータを数値型のデータで表現することができます。興味がある方は実装してみて下さい。

プログラムの修正方法

これまで はゲーム盤の 手番とマスなどのデータ を、下記のプログラムのように Marubatsu クラスの下記の クラス属性に記録 していました。

class Marubatsu:
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "x"
    DRAW = "draw"
    PLAYING = "playing"

手番とマスのデータ クラス属性
空のマス EMPTY
〇 の手番とマーク CIRCLE
× の手番とマーク CROSS
引き分けの状態 DRAW
ゲーム中の状態 PLAYING

Marubatsu クラスの上記の 手番とマスのクラス属性 の値を 数値型のデータに変更 するという方法も考えられますが、そのように変更してしまうと これから定義する NpIntBoard だけでなく、これまでに定義した ListBoard などのクラス でも手番とマスのデータが 整数型のデータで表現される ように変更されてしまうという問題が発生してしまいます。

そこで、手番とマスのデータの記録 を Marubatsu クラスのクラス属性から、ゲーム盤を表すクラスのクラス属性に変更 することにします。そのようにすることで、ゲーム盤を表すクラスごと に、手番とマスのデータを 異なるデータで表現できる ようになります。

なお、引き分けゲーム中 であることを表す Marubatsu クラスのクラス属性である DRAWPLAYING は、ゲーム盤のデータとして記録しません。そのため、ゲーム盤を表すデータによって 変更する必要がない ので、Marubatsu クラスの クラス属性のまま にします。

下記は 変更後のクラス属性を定義するクラス とその値を表す表です。

手番とマスのデータ クラス属性 定義するクラス
空のマス EMPTY ゲーム盤を表すクラス クラスによって異なる
〇 の手番とマーク CIRCLE ゲーム盤を表すクラス クラスによって異なる
× の手番とマーク CROSS ゲーム盤を表すクラス クラスによって異なる
引き分けの状態 DRAW Marubatsu クラス "draw"
ゲーム中の状態 PLAYING Marubatsu クラス "playing"

上記の修正を行うと、これまでに記述したプログラムの多くを修正する必要がある ので大変ですが、今後の記事で手番とマスのデータを様々なデータで表現する方法を紹介する際に必要なので、頑張って修正することにします。

この後でプログラムの修正箇所について説明しますが、修正箇所が非常に多い ので今回の記事では修正したプログラムを JupyterLab ので実行するのではなく、marubatsu.py などのファイルのほうにその修正を直接反映 させることにします。

手番とマスのデータを Marubatsu クラスのクラス属性である EMPTYCIRCLECROSS に代入したのは以前の記事で説明したように、後からそれらのデータを簡単に変更できるようにするためだったのですが、上記の理由から Marubatsu クラスのクラス属性としたのはあまり良くない判断でした。

筆者の見通しが甘かったため大量の修正を行う必要が生じましたが、このように、最初は良いと思っていたことが後で良くないことが判明することは良くあることだと思います。なお、余りにも修正箇所が多すぎる場合や、プログラムの修正が困難な場合などは、最初から設計をやり直したほうが良い場合もあります。

ゲーム盤を表すクラスの修正

NpIntBoard クラスを定義する前に、これまでに定義した ゲーム盤を表すクラス に対して、下記の修正を行います。

  • EMPTYCIRCLECROSS という クラス属性 に手番とマスを表すデータを代入する
  • Marubatsu.EMPTYMarubatsu.CIRCLEMarubatsu.CROSS をそれぞれ self.EMPTYself.CIRCLEself.CROSS に修正 する

下記は ListBoard クラスをそのように修正したプログラムの一部です。長くなるので省略しますが、残りの部分に対しても同様の修正 を行います。ただし、ListBoard クラスには PLAYINGDRAW 属性は存在しない ので、judge メソッド内の Marubatsu.PLAYINGMarubatsu.DRAW を修正してはいけない 点に注意が必要です。

  • 2 ~ 4 行目EMPTYCIRCLECROSS のクラス属性に値を代入する
  • 9、12、13、16、17、20、21 行目Marubatsu.EMPTYMarubatsu.CIRCLEMarubatsu.CROSS をそれぞれ self.EMPTYself.CIRCLEself.CROSS に修正する
 1  class ListBoard(Board):
 2      EMPTY = "."
 3      CIRCLE = "o"
 4      CROSS = "x"
 5  
 6      def __init__(self, board_size:int=3, count_linemark:bool=False):
 7          self.BOARD_SIZE = board_size
 8          self.count_linemark = count_linemark
 9          self.board = [[self.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
10          if self.count_linemark:
11              self.rowcount = {
12                  self.CIRCLE: [0] * self.BOARD_SIZE,
13                  self.CROSS: [0] * self.BOARD_SIZE,
14              }
15              self.colcount = {
16                  self.CIRCLE: [0] * self.BOARD_SIZE,
17                  self.CROSS: [0] * self.BOARD_SIZE,
18              }
19              self.diacount = {
20                  self.CIRCLE: [0] * 2,
21                  self.CROSS: [0] * 2,
22              }

行番号のないプログラム
class ListBoard(Board):
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "."

    def __init__(self, board_size:int=3, count_linemark:bool=False):
        self.BOARD_SIZE = board_size
        self.count_linemark = count_linemark
        self.board = [[self.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
        if self.count_linemark:
            self.rowcount = {
                self.CIRCLE: [0] * self.BOARD_SIZE,
                self.CROSS: [0] * self.BOARD_SIZE,
            }
            self.colcount = {
                self.CIRCLE: [0] * self.BOARD_SIZE,
                self.CROSS: [0] * self.BOARD_SIZE,
            }
            self.diacount = {
                self.CIRCLE: [0] * 2,
                self.CROSS: [0] * 2,
            }

修正箇所
class ListBoard(Board):
+   EMPTY = "."
+   CIRCLE = "o"
+   CROSS = "x"

    def __init__(self, board_size:int=3, count_linemark:bool=False):
        self.BOARD_SIZE = board_size
        self.count_linemark = count_linemark
-       self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
+       self.board = [[self.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
        if self.count_linemark:
            self.rowcount = {
-               Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
+               self.CIRCLE: [0] * self.BOARD_SIZE,
-               Marubatsu.CROSS: [0] * self.BOARD_SIZE,
+               self.CROSS: [0] * self.BOARD_SIZE,
            }
            self.colcount = {
-               Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
+               self.CIRCLE: [0] * self.BOARD_SIZE,
-               Marubatsu.CROSS: [0] * self.BOARD_SIZE,
+               self.CROSS: [0] * self.BOARD_SIZE,
            }
            self.diacount = {
-               Marubatsu.CIRCLE: [0] * 2,
+               self.CIRCLE: [0] * 2,
-               Marubatsu.CROSS: [0] * 2,
+               self.CROSS: [0] * 2,
            }

プログラムは省略しますが、List1dBoardArrayBoardNpBoard クラスに対しても同様の修正を行い、marubatsu.py に反映させました。

なお、上記の修正は下記の手順で行うと良いでしょう1

  • Ctrl + H で下図の VSCode の置換機能を呼び出し、上のテキストボックスに Marubatsu. を、下のテキストボックスに self. を入力する。. を入れるのは class Marubatsu: などの、置換してはいけない Marubatsu が検索されないようにするためである
  • ↓、↑ ボタンをクリックして Marubatsu. を検索し、置換すべき内容であることを確認できた場合は下のテキストボックスの右にある の置換ボタンをクリックして置換する。Marubatsu.DRAW など置換してはいけない場合は ↓ をクリックして次の候補を検索する。その際に、その右にある「すべて置換ボタン」をクリックすると置換してはいけない Marubatsu. まで self. に置換されてしまう点に注意すること

Marubatsu クラスの修正

Marubatsu クラスに対しては、下記の修正を行う必要があります。

  1. クラス属性 EMPTYCIRCLECROSS を削除 する
  2. Marubatsu クラスの インスタンスの EMPTYCIRCLECROSS 属性に ゲーム盤を表すクラスの同名のクラス属性の値を代入 する2
  3. Marubatsu.EMPTYMarubatsu.CIRCLEMarubatsu.CROSSMarubatsu.DRAWMarubatsu.PLAYINGMarubatsuself に修正 する

上記の 2 の修正 は必須ではありませんが、下記の理由から修正を行いました。

  • 2 の修正を行わない場合 は 3 の修正で Marubatsuself.board に修正 する必要がある
  • 2 の修正を行う ことで 3 の修正で Marubatsuself に修正 することができるため、修正の作業が容易 になる。また、self.CIRCLE などの処理時間self.board.CIRCE と比較すると ほんの少しではあるが短くなる

先程のゲーム盤を表す ListBoard クラスなどを修正する際は、ListBoard クラスには DRAWPLAYING 属性が存在しないので Marubatsu.DRAWself.DRAW のように修正してはいけないと説明しましたが、Marubatsu クラスの修正を行う場合 は下記の理由から Marubatsu.DRAWMarubatsu.PLAYING に対しても Marubatsuself に修正する ことにします。

  • Marubatsu クラスにはクラス属性として DRAWPLAYING が存在する
  • 以前の記事で説明したように、クラス属性と同じ名前の属性がインスタンスに存在しない場合は、インスタンスからクラス属性を参照することができる。従って、Marubatsu クラスのインスタンスである mb に対して mb.DRAWmb.PLAYING を記述すると、クラス属性である Marubatsu.DRAWMarubatsu.PLAYING が参照されるので、そのように修正しても問題は発生しない
  • VSCode の置換機能を利用して修正する際に Marubatsu.DRAWMarubatsu.PLAYING を置換しないように気を付けるのは面倒

下記は上記の 2 の修正を行うプログラムで、initialize_board メソッドで board 属性にゲーム盤を表すデータを代入した後 で、EMPTYCIRCLECROSS 属性に ゲーム盤を表すデータの同名のクラス属性を代入 しています。

1      def initialize_board(self):
2          self.board = self.boardclass(self.BOARD_SIZE, *self.args, **self.kwargs)
3          self.EMPTY = self.board.EMPTY
4          self.CIRCLE = self.board.CIRCLE
5          self.CROSS = self.board.CROSS
行番号のないプログラム
    def initialize_board(self):
        self.board = self.boardclass(self.BOARD_SIZE, *self.args, **self.kwargs)
        self.EMPTY = self.board.EMPTY
        self.CIRCLE = self.board.CIRCLE
        self.CROSS = self.board.CROSS
修正箇所
    def initialize_board(self):
        self.board = self.boardclass(self.BOARD_SIZE, *self.args, **self.kwargs)
+       self.EMPTY = self.board.EMPTY
+       self.CIRCLE = self.board.CIRCLE
+       self.CROSS = self.board.CROSS

3 の修正は多数存在しますが、行う修正は置換機能を利用して地道に行えば良いので修正箇所については省略します。

Marubatsu クラスのクラス属性を利用していたプログラムの修正

これまでのプログラムで Marubatsu クラスの EMPTYCIRCLECROSSPLAYINGDRAW のクラス属性を 利用していたプログラムを修正 する必要があります。具体的には Marubatsu.EMPTYMarubatsu.CIRCLEMarubatsu.CROSSMarubatsu.PLAYINGMarubatsu.EMPTY をそれぞれ Marubatsu クラスのインスタンスの EMPTYCIRCLECROSSPLAYINGDRAW 属性に修正する必要があります3

例えば、ai.py の ai1s の場合は、下記のプログラムの 5 行目で mb に代入された Marubatsu クラスのインスタンスのマークが空であるかどうかを比較しているので、Marubatsu.EMPTYmb.EMPTY に修正 します。

1  @ai_by_candidate
2  def ai1(mb, debug):
3      for y in range(mb.BOARD_SIZE):
4          for x in range(mb.BOARD_SIZE):
5              if mb.board.getmark(x, y) == mb.EMPTY:
6                  return [mb.board.xy_to_move(x, y)]
修正箇所
@ai_by_candidate
def ai1(mb, debug):
    for y in range(mb.BOARD_SIZE):
        for x in range(mb.BOARD_SIZE):
-           if mb.board.getmark(x, y) == Marubatsu.EMPTY:
+           if mb.board.getmark(x, y) == mb.EMPTY:
                return [mb.board.xy_to_move(x, y)]

このように、Marubatsu.EMPTY などは、Marubatsu クラスのインスタンスが代入されている 変数名.EMPTY のように修正する必要がある点に注意が必要です。例えば Marubatsu クラスのインスタンスが self.mb に代入されている場合は self.mb.EMPTY に修正します。置換機能を使って修正する場合は、その点に注意しながら行ってください。

以下は上記の修正を行う箇所の一覧です。

  • marubatsu.py
    • Marubatsu_GUI クラスの update_gui
    • Marubatsu_GUI クラスの draw_board
  • ai.py
    • 多数存在するが、行う修正の種類はそれほど多くはない。なお、ai_mmdfs などのゲーム木を探索する AI の関数内で定義されている mm_searchab_search 内では Marubatsu.mborig. に、mm_search などの外では Marubatsu.mb. に修正する必要がある点に注意すること
  • tree.py
    • 下記以外は Marubatsu.node.mb. に修正する
    • Mbtree クラスの create_subtree 内の 1 箇所で Marubatsu.childnode.mb. に修正する
    • Mbtree_Anim クラスの update_abupdate_frameinfo 内では Marubatsu.self.selectednode.mb に修正する
  • util.py
    • Check_Solved クラスの is_weakly_solved_r メソッド内の Marubatsu.node.mb. に修正する

ただし、この後で説明する部分に対しては、上記以外の方法で修正を行う必要があります。

Marubatsu_GUI クラスの draw_markdraw_board メソッドの修正

Marubatsu_GUI クラスの draw_mark メソッドは、下記のプログラムのように 以前の記事で説明した @staticmethod のデコレータを利用して 静的メソッド として定義されています。

1  @staticmethod
2  def draw_mark(ax, x, y, mark, color="black", lw=2):
3      if mark == Marubatsu.CIRCLE:
4          circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=lw)
5          ax.add_artist(circle)
6      elif mark == Marubatsu.CROSS:
7          ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw=lw)
8          ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw=lw)

上記では 3 行目と 6 行目で仮引数 markMarubatsu.CIRCLE または Marubatsu.CROSS であるかを判定して 〇 または × のマークをゲーム盤に描画 する処理を行っていますが、静的メソッドは仮引数 self を持たない ので、draw_mark の仮引数の中に Marubatsu クラスのインスタンスが代入されているものは存在しません。そのため、このままでは Marubatsu.CIRCLEMarubatsu.CROSS を Marubatsu クラスの インスタンスの CIRCLECROSS 属性に修正 することは できません

この問題を解決するためには、下記のプログラムのように draw_markMarubatsu クラスのインスタンスを代入する仮引数 mb を追加 する必要があります。

  • 2 行目:仮引数 mb を追加した
  • 3、6 行目Marubatsu.mb. に修正した
1  @staticmethod
2  def draw_mark(ax, x, y, mb, mark, color="black", lw=2):
3      if mark == mb.CIRCLE:
4          circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=lw)
5          ax.add_artist(circle)
6      elif mark == mb.CROSS:
7          ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw=lw)
8          ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw=lw)
修正箇所
@staticmethod
-def draw_mark(ax, x, y, mark, color="black", lw=2):
+def draw_mark(ax, x, y, mb, mark, color="black", lw=2):
-   if mark == Marubatsu.CIRCLE:
+   if mark == mb.CIRCLE:
        circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=lw)
        ax.add_artist(circle)
-   elif mark == Marubatsu.CROSS:
+   elif mark == mb.CROSS:
        ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw=lw)
        ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw=lw)

draw_mark の仮引数が変化 したので、draw_mark を呼び出すプログラムを修正 する必要があります。draw_markMarubatsu_GUI クラスの draw_board メソッドからのみ 呼び出されているので、その draw_board を下記のプログラムのように修正します。

  • 7 行目draw_mark を呼び出す際の 4 番目の実引数に mb を追加する
1  @staticmethod
2  def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2):
元と同じなので省略
3      # ゲーム盤のマークを描画する
4      for y in range(mb.BOARD_SIZE):
5          for x in range(mb.BOARD_SIZE):
6              color = "red" if mb.board.xy_to_move(x, y) == mb.last_move else "black"
7              Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb, mb.board.getmark(x, y), color, lw=lw)
元と同じなので省略
修正箇所
@staticmethod
def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2):
元と同じなので省略
    # ゲーム盤のマークを描画する
    for y in range(mb.BOARD_SIZE):
        for x in range(mb.BOARD_SIZE):
            color = "red" if mb.board.xy_to_move(x, y) == mb.last_move else "black"
-           Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb.board.getmark(x, y), color, lw=lw)
+           Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb, mb.board.getmark(x, y), color, lw=lw)
元と同じなので省略

mbtest.py の test_judge の修正

mbtest.py 内で定義されている test_judge 内でも Marubatsu.PLAYING などが記述 されていますが、その記述の後で Marubatsu クラスのインスタンスを作成 しているので、このままでは Marubatsu.PLAYING などを修正できない という問題があります。そこで、下記のプログラムのように Marubatsu クラスのインスタンスを作成した後Marubatsu.PLAYING が記述されていた処理を行う ようすることで Marubatsu.PLAYING を修正できるようになります。

  • 2 行目:Marubatsu クラスのインスタンスを作成して mb に代入する
  • 6、9、12、15 行目Marubatsu.mb. に修正する
 1  def test_judge(testcases=None, debug=False, mbparams={}):
 2      mb = Marubatsu(**mbparams)
 3      if testcases is None:
 4          testcases = {
 5              # 決着がついていない場合のテストケース
 6              mb.PLAYING: [
元と同じなので省略
 7              ],   
 8              # 〇の勝利のテストケース
 9              mb.CIRCLE: [
元と同じなので省略
10              ],
11              # × の勝利のテストケース
12              mb.CROSS: [
元と同じなので省略
13              ],
14              # 引き分けの場合のテストケース
15              mb.DRAW: [
16                  "A1,A2,B1,B2,C2,C1,A3,B3,C3",
17              ], 
18          }
修正箇所
def test_judge(testcases=None, debug=False, mbparams={}):
+   mb = Marubatsu(**mbparams)
    if testcases is None:
        testcases = {
            # 決着がついていない場合のテストケース
-           Marubatsu.PLAYING: [
+           mb.PLAYING: [
元と同じなので省略
            ],   
            # 〇の勝利のテストケース
-           Marubatsu.CIRCLE: [
+           mb.CIRCLE: [
元と同じなので省略
            ],
            # × の勝利のテストケース
-           Marubatsu.CROSS: [
+           mb.CROSS: [
元と同じなので省略
            ],
            # 引き分けの場合のテストケース
-           Marubatsu.DRAW: [
+           mb.DRAW: [
                "A1,A2,B1,B2,C2,C1,A3,B3,C3",
            ], 
        }

util.py の Check_Solved クラスの is_weakly_solved の修正

util.pyCheck_Solved クラスの is_weakly_solved静的メソッド なので先程の draw_mark と同様の問題 があります。こちらの場合は、下記のプログラムの 3 行目で root に代入したゲーム木のルートノードの mb 属性に Marubatsu クラスのインスタンスが代入されている ので、下記のプログラムの 4 行目のように Marubatsu.CIRCLEroot.mb.CIRCLE に修正 することができます。5 行目も同様です。

1  @staticmethod
2  def is_weakly_solved(ai, params=None, verbose=True):
元と同じなので省略
3      root = Check_solved.mbtree.root
4      circle_result = Check_solved.is_weakly_solved_r(root, ai, root.mb.CIRCLE, params, set())
5      cross_result = Check_solved.is_weakly_solved_r(root, ai, root.mb.CROSS, params, set())
元と同じなので省略
修正箇所
@staticmethod
def is_weakly_solved(ai, params=None, verbose=True):
元と同じなので省略
    root = Check_solved.mbtree.root
-   circle_result = Check_solved.is_weakly_solved_r(root, ai, Marubatsu.CIRCLE, params, set())
+   circle_result = Check_solved.is_weakly_solved_r(root, ai, root.mb.CIRCLE, params, set())
-   cross_result = Check_solved.is_weakly_solved_r(root, ai, Marubatsu.CROSS, params, set())
+   cross_result = Check_solved.is_weakly_solved_r(root, ai, root.mb.CROSS, params, set())
元と同じなので省略

修正の確認

数多くの修正を行ったので、修正したプログラムが正しく動作するかどうかを確認 する必要があります。

まず、util.py の benchmark が正しく動作するかどうかを確認することにします。先程説明したように、上記で行った修正は、ai.py などに反映済み なので、下記のプログラムを実行して確認することにします。いずれの対戦の場合も、通算成績が以前の記事と同じ なので、正しい計算を行うことができている ことが確認できます。

from marubatsu import ListBoard, List1dBoard, ArrayBoard, NpBoard
from util import benchmark

for boardclass in [ListBoard, List1dBoard, ArrayBoard, NpBoard]:
    for count_linemark in [False, True]:
        print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
        benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
        print()

実行結果

boardclass: ListBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 13363.96it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:34<00:00, 1445.99it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 18.5 ms ±   2.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: ListBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 13041.36it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:21<00:00, 2326.89it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 19.4 ms ±   3.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: List1dBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 16417.96it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:35<00:00, 1390.08it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 17.7 ms ±   2.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: List1dBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 14520.57it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:21<00:00, 2285.08it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 19.3 ms ±   2.0 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: ArrayBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 14661.93it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:36<00:00, 1355.45it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 19.0 ms ±   1.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: ArrayBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 13761.60it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:23<00:00, 2167.56it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 19.9 ms ±   1.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: NpBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 5972.04it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [01:12<00:00, 692.95it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 50.4 ms ±   2.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: NpBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:06<00:00, 7478.88it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:26<00:00, 1894.42it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 45.0 ms ±   3.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は 以前の記事ListBoard、List1dBoard、ArrayBoard のベンチマークの結果 と、前回の記事で修正を行った NpBoard を利用した ai_abs_dls 結果 に上記の実行結果を加えた表です。下段が上記の実行結果を表します。

boardclass count_linemark ai2 VS ai2 ai14s VS ai2 ai_abs_dls
ListBoard False 14417.97 回/秒
13363.96 回/秒
1414.87 回/秒
1445.99 回/秒
17.9 ms
18.5 ms
ListBoard True 14910.42 回/秒
13041.36 回/秒
2515.18 回/秒
2326.89 回/秒
18.0 ms
19.4 ms
List1dBoard False 16462.19 回/秒
16417.96 回/秒
1410.48 回/秒
1390.08 回/秒
17.0 ms
17.7 ms
List1dBoard True 17134.46 回/秒
14520.57 回/秒
2575.17 回/秒
2285.08 回/秒
17.7 ms
19.3 ms
ArrayBoard False 15494.01 回/秒
14661.93 回/秒
1378.88 回/秒
1355.45 回/秒
18.3 ms
19.0 ms
ArrayBoard True 13786.83 回/秒
13761.60 回/秒
2436.19 回/秒
2167.56 回/秒
18.5 ms
19.9 ms
NpBoard False 5707.22 回/秒
5972.04 回/秒
706.02 回/秒
692.95 回/秒
49.2 ms
50.4 ms
NpBoard True 7997.50 回/秒
7478.88 回/秒
1999.46 回/秒
1894.42 回/秒
43.6 ms
45.0 ms

上記の表から、修正前と修正後で一部を除くと ほぼ同じ処理速度になることが確認 できました。ListBoard と List1dBoard で count_linemarkTrue の場合など、一部の処理が明らかに遅くなっていますが、以前の記事と同じ状況でベンチマークをやり直したところ、今回の記事と同じような処理速度になりましたので、今回の処理速度が遅くなった原因はよくわかりませんでした。プログラムを実行した時のコンピューターの状況が何か異なっていたのかもしれません。何か原因がわかったら説明しようと思います。

gui_play による GUI の処理と AI の関数の確認

実行結果は省略しますが、下記のプログラムで gui_play を実行し、下記のような確認を行いました。

  • 人間どうしの対戦 を行うことで、Marubatsu_GUI クラスの修正が正しく動作 することを確認する。また、Marubatsu_GUI クラスでは Mbtree クラスを使用しているので Mbtree クラスの修正が正しく動作 することも確認できる
  • ai2 gui_play で選択できる すべての AI との対戦を行うこと で、gui_play で選択できる AI の関数が動作することを確認 する
from util import gui_play()

gui_play()

興味と余裕がある方は benchmarkgui_play で確認しなかった他の AI の関数について正しく処理を行うことができるかどうかについて確認してみて下さい。

test_judge の確認

下記のプログラムで先程の benchmark と同じ条件で test_judge が正しく動作 することを確認します。実行結果からいずれの場合も正しく動作することが確認できました。

from mbtest import test_judge

for boardclass in [ListBoard, List1dBoard, ArrayBoard, NpBoard]:
    for count_linemark in [False, True]:
        print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
        test_judge(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
        print()

実行結果

boardclass: ListBoard, count_linemark False
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

boardclass: ListBoard, count_linemark True
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

boardclass: List1dBoard, count_linemark False
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

boardclass: List1dBoard, count_linemark True
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

boardclass: ArrayBoard, count_linemark False
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

boardclass: ArrayBoard, count_linemark True
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

boardclass: NpBoard, count_linemark False
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

boardclass: NpBoard, count_linemark True
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

NpIntBoard の定義

ゲーム盤の手番とマスを表すデータを変更することができるようになったので、先程決めた 下記の表のデータ で ndarray で 手番とマスを表す NpIntBoard クラスを定義 します。

手番とマスのデータ データ
空のマス 0
〇 の手番とマーク 1
× の手番とマーク 2

手番とマスを表すデータ以外基本的には NpBoard クラスと同じ処理を行う ので下記のプログラムのように NpBoard クラスを継承 し、クラス属性 EMPTYCIRCLECROSS に上記の表の値を代入 するように定義します。

class NpIntBoard(NpBoard):
    EMPTY = 0
    CIRCLE = 1
    CROSS = 2

上記の定義後に、下記のプログラムで NpIntBoard を利用する Marubatsu クラスのインスタンスを作成 し、board 属性を print 表示 すると実行結果のように 2 次元の ndarray の すべての要素が空のマスを表す整数型の 0 となっていることが確認できます。

from marubatsu import Marubatsu

mb = Marubatsu(boardclass=NpIntBoard)
print(mb.board.board)

実行結果

[[0 0 0]
 [0 0 0]
 [0 0 0]]

NpIntBoard クラスの問題と修正

上記の実行結果から正しく動作しているように見えるかもしれませんが、NpIntBoard には いくつかの問題 があります。その問題について少し考えてみて下さい。

一つ目の問題点の修正

一つ目の問題点は下記のプログラムのように print で上記の mb を表示 しようとすると エラーが発生する というものです。

print(mb)

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 print(mb)

File c:\Users\ys\ai\marubatsu\201\marubatsu.py:931, in Marubatsu.__str__(self)
    928 def __str__(self):
    929     # ゲームの決着がついていない場合は、手番を表示する
    930     if self.status == self.PLAYING:
--> 931         text = "Turn " + self.turn + "\n"
    932     # 決着がついていれば勝者を表示する
    933     else:
    934         text = "winner " + self.status + "\n"

TypeError: can only concatenate str (not "int") to str

エラーメッセージから、print(mb) を実行した際に呼び出される Marubatsu クラスの __str__ メソッド内の text = "Turn " + self.turn + "\n" の処理で 整数型(int)のデータと 文字列型(str)のデータを + 演算子で結合(concatenate)しようとしたためエラーが発生していることが確認できます。実際に self.turn には 手番を表すデータが代入 されおり、先程 手番を表すデータを整数型のデータに変更 したため、整数型と文字列型のデータを結合 しようとしています。

また、上記のエラーが発生したため実行されていませんが、__str__ メソッドでは 決着がついている場合 は下記のプログラムが実行され、上記と同様の理由〇 または × が勝利していた場合数値型と文字列型のデータが結合 されるため エラーが発生 します。

text = "winner " + self.status + "\n"

__str__ メソッドではさらに、下記のプログラムで ゲーム盤を表す文字列を計算 する際に マークを表すデータを += 演算子で計算 するという処理を行っており、この部分が実行された場合も数値型と文字列型のデータが結合されるため エラーが発生 します。

text += self.board.getmark(x, y)

今回の記事で 手番とマスを表すデータ任意のデータに変更できる ようにしましたが、print で表示する手番とマーク常に .ox を表示 すればよいでしょう。また 引き分けの場合 はこれまで通り "draw" という文字列を表示 すればよいでしょう。そこで、下記のようにプログラムを修正することにします。

  • 手番とマスを表す文字列 はゲーム盤を表すクラスに関わらず 共通なのでMarubatsu クラスの EMPTY_STRCIRCLE_STRCROSS_STR というクラス属性に代入 する
  • ゲーム盤を表すクラス手番とマークを表す文字列の対応表 を表す dict が代入 された MARK_TABLE という クラス属性を追加 する。その際に、ゲームの決着がついた局面の表示 を行うプログラムを 簡潔に記述できるようにするため引き分けを表すデータも MARK_TABLE に記録 することにする

具体的には Marubatsu クラスに下記の 2 ~ 4 行目のプログラムを追加します。

1  class Marubatsu:
2      EMPTY_STR = "."
3      CIRCLE_STR = "o"
4      CROSS_STR = "x"
5      DRAW = "draw"
6      PLAYING = "playing"    

クラスを定義し直すのは大変なので、今回の記事では下記のプログラムを実行し、上記の修正は marubatsu.py のほうに行うことにします。

Marubatsu.EMPTY_STR = "."
Marubatsu.CIRCLE_STR = "o"
Marubatsu.CROSS_STR = "x"

また、NpIntBoard クラスの場合は、下記のプログラムのように クラス属性 MARK_TABLE を追加 します。MARK_TABLEMarubatsu.DRAW のキーの値には Marubatsu.DRAW を設定 します。その理由については、下記の __str__ メソッドの説明を見て下さい。

class NpIntBoard(NpBoard):
    EMPTY = 0
    CIRCLE = 1
    CROSS = 2
    MARK_TABLE = {
        EMPTY: Marubatsu.EMPTY_STR,
        CIRCLE: Marubatsu.CIRCLE_STR,
        CROSS: Marubatsu.CROSS_STR,
        Marubatsu.DRAW: Marubatsu.DRAW,       
    }

なお、クラス属性への値の代入処理クラスの定義が実行された際に行われる ので、上記のように Marubatsu クラスのクラス属性の値を NpIntBoard クラスのクラス属性に代入する場合は、Marubatsu クラスの定義よりも後に NpIntBoard クラスを定義 する必要があります。そのため、marubatsu.py の中の Marubatsu クラスの定義ゲーム盤を表すクラスの定義よりも前に移動する必要 があります。

次に、Marubatsu クラスの __str__ メソッドを下記のプログラムのように修正します。

  • 3 行目self.turnself.board.MARK_TABLE[self.turn] に修正することでゲーム中の場合の手番を表すマークを表示する
  • 6 行目self.statusself.board.MARK_TABLE[self.status] に修正することで決着がついた場合の状態を表示する。引き分けを表すデータを MARK_TABLE に入れたことで、〇 の勝利、× の勝利、引き分けのいずれの場合でも同じプログラムで表示を行える
  • 10、12、14 行目:元のプログラムでは 12、14 行目で text にマークを結合していたが、修正後は 10 行目では結合するマークを表す文字列を mark に代入し、12、14 行目で必要に応じて大文字に変換してから text に結合するようにした
 1  def __str__(self):
 2      if self.status == self.PLAYING:
 3          text = "Turn " + self.board.MARK_TABLE[self.turn] + "\n"
 4      # 決着がついていれば勝者を表示する
 5      else:
 6          text = "winner " + self.board.MARK_TABLE[self.status] + "\n"
 7      for y in range(self.BOARD_SIZE):
 8          for x in range(self.BOARD_SIZE):
 9              lastx, lasty = self.board.move_to_xy(self.last_move)
10              mark = self.board.MARK_TABLE[self.board.getmark(x, y)]
11              if x == lastx and y == lasty:
12                  text += mark.upper()
13              else:
14                  text += mark
15          text += "\n"
16      return text
17  
18  Marubatsu.__str__ = __str__
行番号のないプログラム
def __str__(self):
    if self.status == self.PLAYING:
        text = "Turn " + self.board.MARK_TABLE[self.turn] + "\n"
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.board.MARK_TABLE[self.status] + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            lastx, lasty = self.board.move_to_xy(self.last_move)
            mark = self.board.MARK_TABLE[self.board.getmark(x, y)]
            if x == lastx and y == lasty:
                text += mark.upper()
            else:
                text += mark
        text += "\n"
    return text

Marubatsu.__str__ = __str__
修正箇所
def __str__(self):
    if self.status == self.PLAYING:
-       text = "Turn " + self.turn + "\n"
+       text = "Turn " + self.board.MARK_TABLE[self.turn] + "\n"
    # 決着がついていれば勝者を表示する
    else:
-       text = "winner " + self.status + "\n"
+       text = "winner " + self.board.MARK_TABLE[self.status] + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            lastx, lasty = self.board.move_to_xy(self.last_move)
+           mark = self.board.MARK_TABLE[self.board.getmark(x, y)]
            if x == lastx and y == lasty:
-               text += self.board.getmark(x, y).upper()
+               text += mark.upper()
            else:
-               text += self.board.getmark(x, y)            
+               text += mark
        text += "\n"
    return text

Marubatsu.__str__ = __str__

上記の修正後に下記のプログラムで 〇 の手番× の手番〇 の勝利× の勝利引き分け の局面を表示すると、実行結果から いずれの場合も print(mb) が正しく表示される ことが確認できました。

mb = Marubatsu(boardclass=NpIntBoard)
# 〇 の手番の局面
print(mb)
# × の手番の局面
mb.cmove(0, 0)
print(mb)
# 〇 の勝利の局面
mb.cmove(1, 0)
mb.cmove(0, 1)
mb.cmove(1, 1)
mb.cmove(0, 2)
print(mb)
# × の勝利の局面
mb = Marubatsu(boardclass=NpIntBoard)
mb.cmove(0, 0)
mb.cmove(1, 0)
mb.cmove(0, 1)
mb.cmove(1, 1)
mb.cmove(2, 0)
mb.cmove(1, 2)
print(mb)
# 引き分けの局面
mb = Marubatsu(boardclass=NpIntBoard)
mb.cmove(0, 0)
mb.cmove(1, 0)
mb.cmove(0, 1)
mb.cmove(1, 1)
mb.cmove(1, 2)
mb.cmove(0, 2)
mb.cmove(2, 0)
mb.cmove(2, 1)
mb.cmove(2, 2)
print(mb)

実行結果

Turn o
...
...
...

Turn x
O..
...
...

winner o
ox.
ox.
O..

winner x
oxo
ox.
.X.

winner draw
oxo
oxx
xoO

他のゲーム盤を表すクラスの修正

他のゲーム盤を表すクラス に対しても下記のプログラムのように EMPTY などのクラス属性を設定する必要 があります。今回の記事ではこの後で ListBoard クラスなどを利用しないので下記の修正を行ったプログラムは実行せず、marubatsu.py に反映させることにします。

class ListBoard:
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "x"
    MARK_TABLE = {
        EMPTY: Marubatsu.EMPTY_STR,
        CIRCLE: Marubatsu.CIRCLE_STR,
        CROSS: Marubatsu.CROSS_STR,
        Marubatsu.DRAW: Marubatsu.DRAW,       
    }

二つ目の問題点の修正

二つ目の問題は下記のプログラムのように board_to_str メソッドを呼び出すと エラーが発生 するというものです。

print(mb.board_to_str())

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 print(mb.board_to_str())

File c:\Users\ys\ai\marubatsu\201\marubatsu.py:952, in Marubatsu.board_to_str(self)
    945 def board_to_str(self) -> str:
    946     """board 属性の要素を連結した文字列を計算して返す
    947     
    948     Returns:
    949         board 属性の要素を連結した文字列
    950     """
--> 952     return self.board.board_to_str()

File c:\Users\ys\ai\marubatsu\201\marubatsu.py:700, in NpBoard.board_to_str(self)
    699 def board_to_str(self):
--> 700     return "".join(self.board.flatten().tolist())

TypeError: sequence item 0: expected str instance, int found

エラーメッセージから、"".join(self.board.flatten().tolist()) によって 結合するデータ が文字列型ではない 整数型であることが原因 であることがわかります。この問題の 原因は一つ目の問題と同じ なので、NpIntBoard クラスの board_to_str メソッドを下記のプログラムのように修正します。なお、ListBoard などの 他のゲーム盤を表すクラス手番とマークを文字列で表現 しているので board_to_str メソッドを 修正する必要はありません

  • 2 行目:ゲーム盤を表す 2 次元の ndarray を 1 次元の list に変換する。元のプログラムではこのデータに対して "".join で要素を結合していた
  • 3、4 行目self.MARK_TABLE を利用して、各マスのマークを文字列に変換した list を作成し、その list に対して "".join で要素を結合する
1  def board_to_str(self):
2      numlist = self.board.flatten().tolist()
3      strlist = [self.MARK_TABLE[mark] for mark in numlist]
4      return "".join(strlist)
5  
6  NpIntBoard.board_to_str = board_to_str
行番号のないプログラム
def board_to_str(self):
    numlist = self.board.flatten().tolist()
    strlist = [self.MARK_TABLE[mark] for mark in numlist]
    return "".join(strlist)

NpIntBoard.board_to_str = board_to_str
修正箇所
def board_to_str(self):
-   numlist = self.board.flatten().tolist()
-   strlist = [self.MARK_TABLE[mark] for mark in numlist]
-   return "".join(strlist)
+   return "".join(self.board.flatten().tolist())

NpIntBoard.board_to_str = board_to_str

numpy の関数を利用して上記と同様の処理を行うこともできますが、〇× ゲームのような要素が少ない ndarray の場合は処理速度が遅くなる点と、次回の記事で別の方法を紹介する予定なので採用を見送りました。

上記の修正後に下記のプログラムを実行すると、実行結果から エラーが発生しなくなった ことが確認できます。

print(mb.board_to_str())

実行結果

ooxxxooxo

ベンチマークの実行

下記のプログラムで NpIntBoard を利用した場合の ベンチマークを実行 することにします。

boardclass = NpIntBoard
for count_linemark in [False, True]:
    print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
    benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
    print()

実行結果

boardclass: NpIntBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:06<00:00, 7185.05it/s]
count     win    lose    draw
    1   29454   14352    6194
    2   14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
    1   58.9%   28.7%   12.4%
    2   28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [01:09<00:00, 723.75it/s]
count     win    lose    draw
    1   49446       0     554
    2   44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
    1   98.9%    0.0%    1.1%
    2   88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 51.7 ms ±   2.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: NpIntBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:05<00:00, 9820.80it/s] 
count     win    lose    draw
    1   29454   14352    6194
    2   14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
    1   58.9%   28.7%   12.4%
    2   28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:24<00:00, 2030.51it/s]
count     win    lose    draw
    1   49446       0     554
    2   44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
    1   98.9%    0.0%    1.1%
    2   88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 48.9 ms ±   1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は 先ほどの NpBoard を利用した場合のベンチマークの結果と、上記の NpIntBoard を利用した場合の ベンチマークの結果 をまとめた表です。

boardclass count_linemark ai2 VS ai2 ai14s VS ai2 ai_abs_dls
NpBoard False 5972.04 回/秒 692.95 回/秒 50.4 ms
NpIntBoard False 7185.05 回/秒 723.75 回/秒 51.7 ms
NpBoard True 7478.88 回/秒 1894.42 回/秒 45.0 ms
NpIntBoard True 9820.80 回/秒 2030.51 回/秒 48.9 ms

表から、ai2 VS ai2ai14s VS ai2処理速度が向上 していることが確認できます。その理由についてはこの後で説明します。

一方、ai_abs_dls の処理速度は若干ですが低下します。その理由は、ai_abs_dls の処理で 頻繁に呼び出される board_to_str の処理が、先程の修正によって マークを表す数値を文字列型のデータに変換 する処理を行う必要が生じた 分だけ処理速度が遅くなった からです。

下記は NpBoardNpIntBoard のそれぞれを利用した場合の board_to_str の処理速度を計測 するプログラムです。すべてのマークを文字列に変換する処理を行う ように、いくつかのマスに着手 を行いました。実行結果から NpIntBoard クラスの board_to_str の処理時間のほうが 約 2 倍ほどかかる ことが確認できます。

mb1 = Marubatsu(boardclass=NpBoard)
mb1.move(0, 0)
mb1.move(1, 0)
mb1.move(2, 0)
%timeit mb1.board.board_to_str()
mb2 = Marubatsu(boardclass=NpIntBoard)
mb2.move(0, 0)
mb2.move(1, 0)
mb2.move(2, 0)
%timeit mb2.board.board_to_str()

実行結果

790 ns ± 16.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
1.46 μs ± 44.5 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

この問題を改善する方法については次回の記事で紹介する予定です。

ndarray の文字列型のデータと数値型のデータの処理速度

numpy は名前に数値を表す number がついていることからわかるように、数値型のデータの計算が得意 です。そのため、数値型の ndarray のほうが文字列型の ndarray 場合よりも 一般的に処理速度が高速 になります。

例えば、下記のプログラムのように == 演算子 を利用して 各要素の比較 を行う処理は実行結果のように 数値型の ndarray である mb2 のほうが 処理速度が若干速くなります

%timeit mb1.board.board == mb1.CIRCLE
%timeit mb2.board.board == mb2.CIRCLE

実行結果

936 ns ± 18 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
822 ns ± 21.3 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

要素の数が多くなる と、例えば下記のプログラムの 10 × 10 の 2 次元の ndarray の場合の実行結果からわかるように その差がさらに開きます

import numpy as np

%timeit np.full((10, 10), ".") == "."
%timeit np.full((10, 10), 0) == 0

実行結果

3.53 μs ± 65.9 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
2.23 μs ± 98.6 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

Marubatsu クラスの __init__ メソッドの修正

先程 marubatsu.py の中の Marubatsu クラスの定義 を ListBoard などの ゲーム盤を表すクラスより前に移動する必要がある と説明しましたが、そのようにすると 修正した marubatsu.py から Marubatsu クラスをインポートすると NameError: name 'ListBoard' is not defined という エラーが発生する ことが判明しました。

そのようなエラーが発生する原因は、下記の Marubatsu クラスの __init__ メソッドの定義 にあります。

def __init__(self, boardclass=ListBoard, board_size: int=3, *args, **kwargs):
    self.boardclass = boardclass

エラーが発生する原因は以下の通りです。

  • __init__ メソッドでは仮引数 boardclass のデフォルト値として ListBoard が設定 されている
  • 関数の デフォルト値以前の記事で説明したように、関数の定義を実行した際にデフォルト値を管理するオブジェクトに記録 される
  • さきほど Marubatsu クラスの定義を ListBoard クラスの定義の前に移動したので、__init__ メソッドの定義を実行した際 には、ListBoard クラスはまだ定義されていない ので、'ListBoard' is not defined というエラーが発生する

この問題を解決する方法として、下記のプログラムのように 仮引数 boardclass のデフォルト値を None に修正し、__init__ メソッドの中で boardclassNone の場合に ListBoard を代入 するという方法があります。__init__ メソッドのブロックの処理__init__ メソッドを 定義した際には実行されない のでこのエラーは発生しなくなります。

def __init__(self, boardclass=None, board_size: int=3, *args, **kwargs):
    if boardclass is None:
        boardclass = ListBoard
    self.boardclass = boardclass

上記の修正を marubatsu.py に対して行うことにします。

今回の記事のまとめ

今回の記事では 手番とマークを異なる方法で表現できるようにする ための プログラムの修正 を行い、数値型 のデータで 手番とマークを表現 する NpIntBoard クラスを定義 しました。一般的に ndarray数値型のデータ のほうが文字列型のデータよりも 高速に処理を行うことができる ため、NpIntBoard を利用した場合のほうが NpBoard を利用した場合よりも ai2 VS ai2ai14s VS ai2処理速度が若干改善 しました。一方で、board_to_str の処理速度 は数値型のデータを 文字列型のデータに変換する処理が加わる ため 悪化しました が、その問題については次回の記事で改善することにします。

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

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu.py 本記事で更新した marubatsu_new.py

次回の記事

近日公開予定です

  1. 残念ながら以前の記事で紹介したシンボル名の変更の機能は今回の場合は利用できません

  2. 本記事ではまだ説明していない @property というデコレーターを利用することで同様の修正を行うことができますが、処理速度が若干遅くなるので利用しないことにしました

  3. Marubatsu.PLAYINGMarubatsu.DRAW は修正する必要はありませんが、先程説明した理由から置換の作業を簡単にするために修正することにします

0
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
0
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?