目次と前回の記事
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 クラスのクラス属性である DRAW と PLAYING は、ゲーム盤のデータとして記録しません。そのため、ゲーム盤を表すデータによって 変更する必要がない ので、Marubatsu クラスの クラス属性のまま にします。
下記は 変更後のクラス属性を定義するクラス とその値を表す表です。
| 手番とマスのデータ | クラス属性 | 定義するクラス | 値 |
|---|---|---|---|
| 空のマス | EMPTY |
ゲーム盤を表すクラス | クラスによって異なる |
| 〇 の手番とマーク | CIRCLE |
ゲーム盤を表すクラス | クラスによって異なる |
| × の手番とマーク | CROSS |
ゲーム盤を表すクラス | クラスによって異なる |
| 引き分けの状態 | DRAW |
Marubatsu クラス | "draw" |
| ゲーム中の状態 | PLAYING |
Marubatsu クラス | "playing" |
上記の修正を行うと、これまでに記述したプログラムの多くを修正する必要がある ので大変ですが、今後の記事で手番とマスのデータを様々なデータで表現する方法を紹介する際に必要なので、頑張って修正することにします。
この後でプログラムの修正箇所について説明しますが、修正箇所が非常に多い ので今回の記事では修正したプログラムを JupyterLab ので実行するのではなく、marubatsu.py などのファイルのほうにその修正を直接反映 させることにします。
手番とマスのデータを Marubatsu クラスのクラス属性である EMPTY、CIRCLE、CROSS に代入したのは以前の記事で説明したように、後からそれらのデータを簡単に変更できるようにするためだったのですが、上記の理由から Marubatsu クラスのクラス属性としたのはあまり良くない判断でした。
筆者の見通しが甘かったため大量の修正を行う必要が生じましたが、このように、最初は良いと思っていたことが後で良くないことが判明することは良くあることだと思います。なお、余りにも修正箇所が多すぎる場合や、プログラムの修正が困難な場合などは、最初から設計をやり直したほうが良い場合もあります。
ゲーム盤を表すクラスの修正
NpIntBoard クラスを定義する前に、これまでに定義した ゲーム盤を表すクラス に対して、下記の修正を行います。
-
EMPTY、CIRCLE、CROSSという クラス属性 に手番とマスを表すデータを代入する -
Marubatsu.EMPTY、Marubatsu.CIRCLE、Marubatsu.CROSSをそれぞれself.EMPTY、self.CIRCLE、self.CROSSに修正 する
下記は ListBoard クラスをそのように修正したプログラムの一部です。長くなるので省略しますが、残りの部分に対しても同様の修正 を行います。ただし、ListBoard クラスには PLAYING と DRAW 属性は存在しない ので、judge メソッド内の Marubatsu.PLAYING と Marubatsu.DRAW を修正してはいけない 点に注意が必要です。
-
2 ~ 4 行目:
EMPTY、CIRCLE、CROSSのクラス属性に値を代入する -
9、12、13、16、17、20、21 行目:
Marubatsu.EMPTY、Marubatsu.CIRCLE、Marubatsu.CROSSをそれぞれself.EMPTY、self.CIRCLE、self.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,
}
略
プログラムは省略しますが、List1dBoard、ArrayBoard、NpBoard クラスに対しても同様の修正を行い、marubatsu.py に反映させました。
なお、上記の修正は下記の手順で行うと良いでしょう1。
- Ctrl + H で下図の VSCode の置換機能を呼び出し、上のテキストボックスに
Marubatsu.を、下のテキストボックスにself.を入力する。.を入れるのはclass Marubatsu:などの、置換してはいけないMarubatsuが検索されないようにするためである - ↓、↑ ボタンをクリックして
Marubatsu.を検索し、置換すべき内容であることを確認できた場合は下のテキストボックスの右にある
の置換ボタンをクリックして置換する。Marubatsu.DRAWなど置換してはいけない場合は ↓ をクリックして次の候補を検索する。その際に、その右にある「すべて置換ボタン」をクリックすると置換してはいけないMarubatsu.までself.に置換されてしまう点に注意すること
Marubatsu クラスの修正
Marubatsu クラスに対しては、下記の修正を行う必要があります。
- クラス属性
EMPTY、CIRCLE、CROSSを削除 する - Marubatsu クラスの インスタンスの
EMPTY、CIRCLE、CROSS属性に ゲーム盤を表すクラスの同名のクラス属性の値を代入 する2 -
Marubatsu.EMPTY、Marubatsu.CIRCLE、Marubatsu.CROSS、Marubatsu.DRAW、Marubatsu.PLAYINGのMarubatsuをselfに修正 する
上記の 2 の修正 は必須ではありませんが、下記の理由から修正を行いました。
-
2 の修正を行わない場合 は 3 の修正で
Marubatsuをself.boardに修正 する必要がある -
2 の修正を行う ことで 3 の修正で
Marubatsuをselfに修正 することができるため、修正の作業が容易 になる。また、self.CIRCLEなどの処理時間 はself.board.CIRCEと比較すると ほんの少しではあるが短くなる
先程のゲーム盤を表す ListBoard クラスなどを修正する際は、ListBoard クラスには DRAW と PLAYING 属性が存在しないので Marubatsu.DRAW を self.DRAW のように修正してはいけないと説明しましたが、Marubatsu クラスの修正を行う場合 は下記の理由から Marubatsu.DRAW と Marubatsu.PLAYING に対しても Marubatsu を self に修正する ことにします。
- Marubatsu クラスにはクラス属性として
DRAWとPLAYINGが存在する -
以前の記事で説明したように、クラス属性と同じ名前の属性がインスタンスに存在しない場合は、インスタンスからクラス属性を参照することができる。従って、Marubatsu クラスのインスタンスである
mbに対してmb.DRAWやmb.PLAYINGを記述すると、クラス属性であるMarubatsu.DRAWやMarubatsu.PLAYINGが参照されるので、そのように修正しても問題は発生しない - VSCode の置換機能を利用して修正する際に
Marubatsu.DRAWとMarubatsu.PLAYINGを置換しないように気を付けるのは面倒
下記は上記の 2 の修正を行うプログラムで、initialize_board メソッドで board 属性にゲーム盤を表すデータを代入した後 で、EMPTY、CIRCLE、CROSS 属性に ゲーム盤を表すデータの同名のクラス属性を代入 しています。
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 クラスの EMPTY、CIRCLE、CROSS、PLAYING、DRAW のクラス属性を 利用していたプログラムを修正 する必要があります。具体的には Marubatsu.EMPTY、Marubatsu.CIRCLE、Marubatsu.CROSS、Marubatsu.PLAYING、Marubatsu.EMPTY をそれぞれ Marubatsu クラスのインスタンスの EMPTY、CIRCLE、CROSS、PLAYING、DRAW 属性に修正する必要があります3。
例えば、ai.py の ai1s の場合は、下記のプログラムの 5 行目で mb に代入された Marubatsu クラスのインスタンスのマークが空であるかどうかを比較しているので、Marubatsu.EMPTY を mb.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
- Marubatsu_GUI クラスの
- ai.py
- 多数存在するが、行う修正の種類はそれほど多くはない。なお、
ai_mmdfsなどのゲーム木を探索する AI の関数内で定義されているmm_searchやab_search内ではMarubatsu.をmborig.に、mm_searchなどの外ではMarubatsu.をmb.に修正する必要がある点に注意すること
- 多数存在するが、行う修正の種類はそれほど多くはない。なお、
- tree.py
- 下記以外は
Marubatsu.をnode.mb.に修正する - Mbtree クラスの
create_subtree内の 1 箇所でMarubatsu.をchildnode.mb.に修正する - Mbtree_Anim クラスの
update_abとupdate_frameinfo内ではMarubatsu.をself.selectednode.mbに修正する
- 下記以外は
- util.py
- Check_Solved クラスの
is_weakly_solved_rメソッド内のMarubatsu.をnode.mb.に修正する
- Check_Solved クラスの
ただし、この後で説明する部分に対しては、上記以外の方法で修正を行う必要があります。
Marubatsu_GUI クラスの draw_mark と draw_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 行目で仮引数 mark が Marubatsu.CIRCLE または Marubatsu.CROSS であるかを判定して 〇 または × のマークをゲーム盤に描画 する処理を行っていますが、静的メソッドは仮引数 self を持たない ので、draw_mark の仮引数の中に Marubatsu クラスのインスタンスが代入されているものは存在しません。そのため、このままでは Marubatsu.CIRCLE と Marubatsu.CROSS を Marubatsu クラスの インスタンスの CIRCLE と CROSS 属性に修正 することは できません。
この問題を解決するためには、下記のプログラムのように draw_mark に Marubatsu クラスのインスタンスを代入する仮引数 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_mark は Marubatsu_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.py の Check_Solved クラスの is_weakly_solved も 静的メソッド なので先程の draw_mark と同様の問題 があります。こちらの場合は、下記のプログラムの 3 行目で root に代入したゲーム木のルートノードの mb 属性に Marubatsu クラスのインスタンスが代入されている ので、下記のプログラムの 4 行目のように Marubatsu.CIRCLE を root.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_linemark が True の場合など、一部の処理が明らかに遅くなっていますが、以前の記事と同じ状況でベンチマークをやり直したところ、今回の記事と同じような処理速度になりましたので、今回の処理速度が遅くなった原因はよくわかりませんでした。プログラムを実行した時のコンピューターの状況が何か異なっていたのかもしれません。何か原因がわかったら説明しようと思います。
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()
興味と余裕がある方は benchmark と gui_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 クラスを継承 し、クラス属性 EMPTY、CIRCLE、CROSS に上記の表の値を代入 するように定義します。
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 で表示する手番とマーク は 常に .、o、x を表示 すればよいでしょう。また 引き分けの場合 はこれまで通り "draw" という文字列を表示 すればよいでしょう。そこで、下記のようにプログラムを修正することにします。
-
手番とマスを表す文字列 はゲーム盤を表すクラスに関わらず 共通なので、Marubatsu クラスの
EMPTY_STR、CIRCLE_STR、CROSS_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_TABLE の Marubatsu.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.turnをself.board.MARK_TABLE[self.turn]に修正することでゲーム中の場合の手番を表すマークを表示する -
6 行目:
self.statusをself.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 ai2 と ai14s VS ai2 の 処理速度が向上 していることが確認できます。その理由についてはこの後で説明します。
一方、ai_abs_dls の処理速度は若干ですが低下します。その理由は、ai_abs_dls の処理で 頻繁に呼び出される board_to_str の処理が、先程の修正によって マークを表す数値を文字列型のデータに変換 する処理を行う必要が生じた 分だけ処理速度が遅くなった からです。
下記は NpBoard と NpIntBoard のそれぞれを利用した場合の 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__ メソッドの中で boardclass が None の場合に 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 ai2 と ai14s VS ai2 の 処理速度が若干改善 しました。一方で、board_to_str の処理速度 は数値型のデータを 文字列型のデータに変換する処理が加わる ため 悪化しました が、その問題については次回の記事で改善することにします。
本記事で入力したプログラム
| リンク | 説明 |
|---|---|
| marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
| marubatsu.py | 本記事で更新した marubatsu_new.py |
次回の記事
近日公開予定です