目次と前回の記事
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 の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
回転と反転を利用した同一局面の計算
下記は util.py で定義した 同一局面の一覧を計算 する calc_sameboard_texts の定義です。
def calc_same_boardtexts(mb, move=None):
data = [ [ 0, 0, 1, 1, -1, 0, 1, 0, -1, 0, 1, 0],
[ 1, -1, 0, 1, 0, -1] * 2,
[ 1, 0, -1, 0, 1, 0, 0, 0, 1, 1, -1, 0],
[ 1, -1, 0, 0, 0, 1] * 2,
[ 0, 1, 0, 1, 0, -1] * 2,
[ 1, 0, -1, 1, -1, 0] * 2,
[ 0, 0, 1, 0, 1, 0] * 2, ]
if move is None:
boardtexts = set([mb.board_to_str()])
else:
boardtexts = { mb.board_to_str(): move }
for xa, xb, xc, ya, yb, yc, xa2, xb2, xc2, ya2, yb2, yc2 in data:
txt = ""
for x in range(mb.BOARD_SIZE):
for y in range(mb.BOARD_SIZE):
txt += mb.board.getmark(xa * 2 + xb * x + xc * y, ya * 2 + yb * x + yc * y)
if move is None:
boardtexts.add(txt)
else:
x, y = mb.board.move_to_xy(move)
x, y = xa2 * 2 + xb2 * x + xc2 * y, ya2 * 2 + yb2 * x + yc2 * y
boardtexts[txt] = mb.board.xy_to_move(x, y)
return boardtext
calc_same_boardtexts では下図の 8 種類の同一局面 を、下図の 左の局面に対して 7 種類の計算を行う ことで求めています。上記の data に代入した list は 左の局面から 2 ~ 7 番目の同一局面を計算 する際に 利用するデータ です。
上記のプログラムは、何を行っているかがわかりづらい プログラムではないかと思います。2 次元の ndarray でゲーム盤を表現した場合は、numpy の関数を利用することで 同一局面を より簡潔で分かりやすい方法 で計算することができるので、その方法を紹介します。
回転と左右の反転による同一局面の計算
以後の説明では、特に表記がない場合の 回転 は 時計回りの回転を表す ことにします。
上図の 8 種類の同一局面のうち、2 ~ 4 番目の局面 は 左の局面を 90 度ずつ回転 した局面です。一方 5 ~ 8 番目の局面 は 最初の局面を左右に反転 させた局面と、その反転した局面を 180 度、90 度、270 度回転 した局面です。従って、〇× ゲーム、オセロ、囲碁のような 正方形のゲーム盤 で 駒に向きがない1ゲームの同一局面は、最初の局面に対して下記の計算を連続して行う ことで求めることができます。
- 90 度回転する
- 90 度回転する
- 90 度回転する
- 左右を反転する
- 90 度回転する
- 90 度回転する
- 90 度回転する
下図は 左の局面 に対して上記の 7 つの計算を連続して行った局面 の図です。
下記の 先程の同一局面の図と比較 すると、左から 2、3、4、6 番目の局面は同じ で、上の 5 番目と下の 7 番目、上の 7 番目と下の 8 番目、上の 8 番目と下の 5 番目 の 局面が同じ であることから、上記の手順で同一局面を計算できる ことが確認できます。
np.rot90 と np.fliplr を利用した同一局面の計算
numpy には 2 次元の ndarray の要素を 90 度時計回りに回転(rotate)した新しい ndarray を計算する np.rot90 という関数が定義されています。np.rot90 と 以前の記事で紹介した 2 次元の ndarray の要素を左右に反転させた新しい ndarray を計算する np.fliplr を利用 することで、下記のプログラムのように すべての同一局面を計算 することができます。
-
4 ~ 8 行目:ndarray でゲーム盤を表現する Marubatsu クラスのインスタンスを作成し、先程の図の左の局面になるように着手を行い、確認のためゲーム盤の表示を行う。なお、NpBoard ではなく Marubatsu クラスのインスタンスを作成した理由は
printでゲーム盤を表示することができるためである -
10、11 行目:
np.rot90を利用してmb.board.boardに代入されている 2 次元の ndarray で表現されたゲーム盤を 90 度回転した ndarray を計算し、mb.board.boardに代入する。その後ゲーム盤を表示する処理を行う - 9 ~ 11、14 ~ 16 行目:上記の 90 度回転する処理を 3 回繰り返す
-
15、16 行目:4 回目の処理で上記と同様の方法で
np.fliplrを利用してゲーム盤を左右に反転する処理を行う
1 from marubatsu import Marubatsu, NpBoard
2 import numpy as np
3
4 mb = Marubatsu(boardclass=NpBoard)
5 mb.cmove(1, 1)
6 mb.cmove(0, 0)
7 mb.cmove(1, 0)
8 print(mb)
9 for _ in range(3):
10 mb.board.board = np.rot90(mb.board.board)
11 print(mb)
12 mb.board.board = np.fliplr(mb.board.board)
13 print(mb)
14 for _ in range(3):
15 mb.board.board = np.rot90(mb.board.board)
16 print(mb)
行番号のないプログラム
from marubatsu import Marubatsu, NpBoard
import numpy as np
mb = Marubatsu(boardclass=NpBoard)
mb.cmove(1, 1)
mb.cmove(0, 0)
mb.cmove(1, 0)
print(mb)
for _ in range(3):
mb.board.board = np.rot90(mb.board.board)
print(mb)
mb.board.board = np.fliplr(mb.board.board)
print(mb)
for _ in range(3):
mb.board.board = np.rot90(mb.board.board)
print(mb)
実行結果
Turn x
xO.
.o.
...
Turn x
..x
.oo
...
Turn x
...
.o.
.ox
Turn x
...
oo.
x..
Turn x
x..
oo.
...
Turn x
.Ox
.o.
...
Turn x
...
.oo
..x
Turn x
...
.o.
xo.
上記の実行結果 と下記の 先程の図を見比べる と、5 番目の局面 が 4 番目の局面を 左右ではなく 上下を反転した局面になっている ことが確認できます。ただし、下図と順番は異なりますが、全体でみると 8 種類の同一局面が表示されている ので、上下の反転処理でも 同一局面を正しく計算できていることが確認 できます。
np.rot90 の詳細については下記のリンク先を参照して下さい。
np.fliplr の補足
np.fliplr によってゲーム盤が左右ではなく 上下が反転する理由 について補足します。
2 次元の ndarray を下記のプログラムで print で表示 すると、実行結果のように print(mb) で表示したゲーム盤と比較して x 座標と y 座標が反転して表示 されます。
print(mb)
print(mb.board.board)
実行結果
Turn x
...
.O.
xo.
[['.' '.' 'x']
['.' 'o' 'o']
['.' '.' '.']]
これは 以前の記事で説明したように、2 次元の list を下記のように プログラムで記述 した際に 最初の添字が縦方向の要素の順番 を、2 つ目の添字が横方向の要素の順番 を表すからです。この性質は 2 次元の ndarray でも変わりません。
board = [
['.', '.', 'x']
['.', 'o', 'o']
['.', '.', '.']]
]
一方、ゲーム盤を表す 2 次元の list や ndarray の 最初の添え字は横方向の x 座標 を、2 番目の添字は縦方向の y 座標 を表すことに決めたため、print(mb) でゲーム盤を表示した場合と、print(mb.board.board) でゲーム盤を表す 2 次元の ndarray を表示した場合では 縦と横が反転した内容が表示される ことになります。
np.fliplr はプログラムで記述した場合と同様に 最初の添字を縦方向、2 番目の添字を横方向 とみなして 左右に反転を行う ため、print(mb) でゲーム盤を表示すると左右ではなく 上下が反転する ことになります。
従って 〇× ゲームのゲーム盤を左右に反転 する場合は 2 次元の ndarray の上下を反転する np.flipud2 を利用する必要がありますが、np.fliplr を利用しても 同一局面を計算できることに変わりはない ので、本記事ではこのまま np.fliplr を利用 することにします。
np.rot90 は 2 次元の ndarray を print で表示した場合 に下記のプログラムのように 反時計回り(counterclockwise)に 90 度回転 する処理を行います。
print(mb.board.board)
print(np.rot90(mb.board.board))
実行結果
[['.' '.' 'x']
['.' 'o' 'o']
['.' '.' '.']]
[['x' 'o' '.']
['.' 'o' '.']
['.' '.' '.']]
そのことは numpy の rot90 のドキュメントに下記のように説明されています。
Rotation direction is from the first towards the second axis. This means for a 2D array with the default k and axes, the rotation will be counterclockwise.
一方 〇×ゲームのゲーム盤として print(mb) で表示 した場合は fliplr の場合と同じ理由で 時計回りに 90 度回転 します。
同一局面に対応する座標の計算
calc_same_boardtexts は 仮引数 move に座標を表すデータを代入すると、同一局面に対応する座標を計算 する処理を行うので、その処理を実装する必要があります。同一局面の計算 を 90 度回転 と 上下の反転 によって行う場合は、同一局面に対応する座標の計算も ゲーム盤を 90 度回転 した場合の座標と、ゲーム盤の上下を反転 した場合の座標を計算します。
上図の 2 ~ 7 番目の同一局面に対応する座標の計算処理 は calc_same_boardtexts 内の下記のプログラムで行っています。この中から ゲーム盤を 90 度回転 した場合と、ゲーム盤の上下を反転3した場合の座標の計算処理を参考にしてプログラムを記述することにします。
data = [ [ 0, 0, 1, 1, -1, 0, 1, 0, -1, 0, 1, 0], # 2 番目の局面を計算するためのデータ
[ 1, -1, 0, 1, 0, -1] * 2, # 3 番目の局面を計算するためのデータ
[ 1, 0, -1, 0, 1, 0, 0, 0, 1, 1, -1, 0], # 4 番目の局面を計算するためのデータ
[ 1, -1, 0, 0, 0, 1] * 2, # 5 番目の局面を計算するためのデータ
[ 0, 1, 0, 1, 0, -1] * 2, # 6 番目の局面を計算するためのデータ
[ 1, 0, -1, 1, -1, 0] * 2, # 7 番目の局面を計算するためのデータ
[ 0, 0, 1, 0, 1, 0] * 2, ]] # 8 番目の局面を計算するためのデータ
略
for xa, xb, xc, ya, yb, yc, xa2, xb2, xc2, ya2, yb2, yc2 in data:
略
x, y = mb.board.move_to_xy(move)
x, y = xa2 * 2 + xb2 * x + xc2 * y, ya2 * 2 + yb2 * x + yc2 * y
boardtexts[txt] = mb.board.xy_to_move(x, y)
上図の 左の局面を 90 度回転 した局面は 2 番目の局面 で、上下を反転 した局面は 6 番目の局面 です。従って、それぞれの局面に対応する座標は、data の 0 番と 4 番の要素 に代入されたデータを x, y = xa2 * 2 + xb2 * x + xc2 * y, ya2 * 2 + yb2 * x + yc2 * y という式に代入 することで計算できます。下記は 90 度回転 した局面に対応する座標と、上下を反転 した局面に対応する 座標を計算する式 です。
-
90 度回転 した局面に対応する座標
-
xa2, xb2, xc2, ya2, yb2, yc2 = 1, 0, -1, 0, 1, 0なので x, y = 1 * 2 + 0 * x + -1 * y, 0 * 2 + 1 * x + 0 * yx, y = 2 - y, x
-
-
上下を反転 した局面に対応する座標
-
xa2, xb2, xc2, ya2, yb2, yc2 = 0, 1, 0, 1, 0, -1なので x, y = 0 * 2 + 1 * x + 0 * y, 1 * 2 + 0 * x + -1 * yx, y = x, 2 - y-
xの値は変わらないのでy = 2 - yのみを計算 すればよい
-
ただし、上記の式 は ゲーム盤のサイズが 3 の場合 の式なので、ゲーム盤の サイズが 3 以外の場合 は 正しい座標を計算できないというバグ があります。上記の式の 2 は ゲーム盤のサイズ - 1 によって計算される値なので、正しい式 は下記のようになります。
-
90 度回転した局面に対応する座標
x, y = mb.BOARD_SIZE - 1 - y, x -
上下を反転した局面に対応する座標
y = mb.BOARD_SIZE - 1 - y
このバグは calc_same_boardtexts 内で同一局面を表す文字列を計算する下記の式でも同様です。
txt += mb.board.getmark(xa * 2 + xb * x + xc * y, ya * 2 + yb * x + yc * y)
そのため、上記の式は正しくは下記のプログラムのようになります。
txt += mb.board.getmark(xa * (mb.board.BOARD_SIZE -1) + xb * x + xc * y,
ya * (mb.board.BOARD_SIZE -1) + yb * x + yc * y)
今回の記事の最後で util.py の calc_same_boardtexts を上記のように修正します。
上記の計算が正しいことを確認 します。print(mb) でゲーム盤を表示した際に、最後に着手したマークは大文字で表示 されます。先ほど の np.rot90 と np.fliplr を利用した 同一局面の計算結果 では、O と o の見た目が似ているのでわかりづらいですが 最初の局面 を print で表示した場合は下記のように 最後に着手した (0, 1) のマスのマークが大文字の O で表示 されますが、それ以降の (0, 1) に対応するマークは小文字の o が表示 されます。
Turn x
xO. # 最後に着手を行った (1, 0) が大文字の O で表示される
.o.
...
Turn x
..x
.oo # (1, 0) を 90 度回転させた (2, 1) が小文字の o で表示されてしまう
...
Turn x
...
.o.
.ox # (1, 0) を 180 度回転させた (1, 2) が小文字の o で表示されてしまう
以下略
これは、直前の着手の座標 を表す last_move の値を計算していない ためなので、下記のプログラムのように先程の式を利用して last_move の値を計算 すると、実行結果のように (0, 1) に対応するマークが大文字の O になる ことから 先程の式が正しい ことが確認できます。
なお、下記のプログラムでは 7 回の繰り返し処理 の中で 4 回目の処理のみを fliplr で、それ以外の処理は rot90 で計算するように修正しました。
-
1 ~ 4 行目:先程の計算で
mb.board.boardの値を別の同一局面を表す値に変更したので、もう一度最初から図の左の局面になるように着手を行う必要がある点に注意すること -
6 行目:
mb.board.move_to_xyを利用して直前の着手の x, y 座標を計算する - 7 行目:7 種類の同一局面を計算する繰り返し処理を行う
-
8 ~ 10 行目:4 回目以外の繰り返し処理ではゲーム盤と (x, y) の座標を 90 度回転する処理を行い、4 回目の処理では上下を反転する処理を行う。なお、
iは 0 から数え始めるので 4 回目の処理の場合のiは3である点に注意すること -
14 行目:
mb.board.xy_to_moveを利用して直前の着手の座標を更新する
1 mb = Marubatsu(boardclass=NpBoard)
2 mb.cmove(1, 1)
3 mb.cmove(0, 0)
4 mb.cmove(1, 0)
5 print(mb)
6 x, y = mb.board.move_to_xy(mb.last_move)
7 for i in range(7):
8 if i != 3:
9 mb.board.board = np.rot90(mb.board.board)
10 x, y = mb.BOARD_SIZE - 1 - y, x
11 else:
12 mb.board.board = np.fliplr(mb.board.board)
13 y = mb.BOARD_SIZE - 1 - y
14 mb.last_move = mb.board.xy_to_move(x, y)
15 print(mb)
行番号のないプログラム
mb = Marubatsu(boardclass=NpBoard)
mb.cmove(1, 1)
mb.cmove(0, 0)
mb.cmove(1, 0)
print(mb)
x, y = mb.board.move_to_xy(mb.last_move)
for i in range(7):
if i != 3:
mb.board.board = np.rot90(mb.board.board)
x, y = mb.BOARD_SIZE - 1 - y, x
else:
mb.board.board = np.fliplr(mb.board.board)
y = mb.BOARD_SIZE - 1 - y
mb.last_move = mb.board.xy_to_move(x, y)
print(mb)
実行結果
Turn x
xO.
.o.
...
Turn x
..x
.oO
...
Turn x
...
.o.
.Ox
Turn x
...
Oo.
x..
Turn x
x..
Oo.
...
Turn x
.Ox
.o.
...
Turn x
...
.oO
..x
Turn x
...
.o.
xO.
calc_same_boardtexts メソッドの定義
これまでは 同一局面の計算 を util.py で定義した calc_same_boardtexts で行っていましたが、異なるゲーム盤を表すクラス で 異なる方法で同一局面の計算を行うことができるようにする ために、calc_same_boardtexts を ゲーム盤を表すクラスのメソッドとして定義 することにします。
Board クラスの抽象メソッドとしての定義
まず、下記のプログラムのようにゲーム盤を表すクラスの基底クラスとなる Board クラスの抽象メソッドとして calc_same_boardtexts を定義 します。
from marubatsu import Board
from abc import abstractmethod
@abstractmethod
def calc_same_boardtexts(self, move=None):
pass
Board.calc_same_boardtexts = calc_same_boardtexts
ListBoard クラスでの定義
下記は ListBoard クラスの calc_same_boardtexts の定義です。行う処理は util.py の calc_same_boardtexts と同じです。
-
3 行目:仮引数
mbをselfに修正した -
12、14、17、18、19、23、25 行目:
mbをselfに修正した -
19 行目:
mb.board.getmarkをself.getmarkに修正した -
23 行目:
mb.board.move_to_xyをself.move_to_xyに修正した -
24 行目:
2を(self.BOARD_SIZE - 1)に修正した -
25 行目:
mb.board.xy_to_moveをself.xy_to_moveに修正した
1 from marubatsu import ListBoard
2
3 def calc_same_boardtexts(self, move=None):
4 data = [ [ 0, 0, 1, 1, -1, 0, 1, 0, -1, 0, 1, 0],
5 [ 1, -1, 0, 1, 0, -1] * 2,
6 [ 1, 0, -1, 0, 1, 0, 0, 0, 1, 1, -1, 0],
7 [ 1, -1, 0, 0, 0, 1] * 2,
8 [ 0, 1, 0, 1, 0, -1] * 2,
9 [ 1, 0, -1, 1, -1, 0] * 2,
10 [ 0, 0, 1, 0, 1, 0] * 2, ]
11 if move is None:
12 boardtexts = set([self.board_to_str()])
13 else:
14 boardtexts = { self.board_to_str(): move }
15 for xa, xb, xc, ya, yb, yc, xa2, xb2, xc2, ya2, yb2, yc2 in data:
16 txt = ""
17 for x in range(self.BOARD_SIZE):
18 for y in range(self.BOARD_SIZE):
19 txt += self.getmark(xa * (self.BOARD_SIZE - 1) + xb * x + xc * y, ya * (self.BOARD_SIZE - 1) + yb * x + yc * y)
20 if move is None:
21 boardtexts.add(txt)
22 else:
23 x, y = self.move_to_xy(move)
24 x, y = xa2 * (self.BOARD_SIZE - 1) + xb2 * x + xc2 * y, ya2 * (self.BOARD_SIZE - 1) + yb2 * x + yc2 * y
25 boardtexts[txt] = self.xy_to_move(x, y)
26 return boardtexts
27
28 ListBoard.calc_same_boardtexts = calc_same_boardtexts
行番号のないプログラム
from marubatsu import ListBoard
def calc_same_boardtexts(self, move=None):
data = [ [ 0, 0, 1, 1, -1, 0, 1, 0, -1, 0, 1, 0],
[ 1, -1, 0, 1, 0, -1] * 2,
[ 1, 0, -1, 0, 1, 0, 0, 0, 1, 1, -1, 0],
[ 1, -1, 0, 0, 0, 1] * 2,
[ 0, 1, 0, 1, 0, -1] * 2,
[ 1, 0, -1, 1, -1, 0] * 2,
[ 0, 0, 1, 0, 1, 0] * 2, ]
if move is None:
boardtexts = set([self.board_to_str()])
else:
boardtexts = { self.board_to_str(): move }
for xa, xb, xc, ya, yb, yc, xa2, xb2, xc2, ya2, yb2, yc2 in data:
txt = ""
for x in range(self.BOARD_SIZE):
for y in range(self.BOARD_SIZE):
txt += self.getmark(xa * (self.BOARD_SIZE - 1) + xb * x + xc * y, ya * (self.BOARD_SIZE - 1) + yb * x + yc * y)
if move is None:
boardtexts.add(txt)
else:
x, y = self.move_to_xy(move)
x, y = xa2 * (self.BOARD_SIZE - 1) + xb2 * x + xc2 * y, ya2 * (self.BOARD_SIZE - 1) + yb2 * x + yc2 * y
boardtexts[txt] = self.xy_to_move(x, y)
return boardtexts
ListBoard.calc_same_boardtexts = calc_same_boardtexts
修正箇所
from marubatsu import ListBoard
-def calc_same_boardtexts(mb, move=None):
+def calc_same_boardtexts(self, move=None):
data = [ [ 0, 0, 1, 1, -1, 0, 1, 0, -1, 0, 1, 0],
[ 1, -1, 0, 1, 0, -1] * 2,
[ 1, 0, -1, 0, 1, 0, 0, 0, 1, 1, -1, 0],
[ 1, -1, 0, 0, 0, 1] * 2,
[ 0, 1, 0, 1, 0, -1] * 2,
[ 1, 0, -1, 1, -1, 0] * 2,
[ 0, 0, 1, 0, 1, 0] * 2, ]
if move is None:
- boardtexts = set([mb.board_to_str()])
+ boardtexts = set([self.board_to_str()])
else:
- boardtexts = { mb.board_to_str(): move }
+ boardtexts = { self.board_to_str(): move }
for xa, xb, xc, ya, yb, yc, xa2, xb2, xc2, ya2, yb2, yc2 in data:
txt = ""
- for x in range(mb.BOARD_SIZE):
+ for x in range(self.BOARD_SIZE):
- for y in range(mb.BOARD_SIZE):
+ for y in range(self.BOARD_SIZE):
- txt += mb.board.getmark(xa * (mb.BOARD_SIZE - 1) + xb * x + xc * y, ya * (mb.BOARD_SIZE - 1) + yb * x + yc * y)
+ txt += self.getmark(xa * (self.BOARD_SIZE - 1) + xb * x + xc * y, ya * (self.BOARD_SIZE - 1) + yb * x + yc * y)
if move is None:
boardtexts.add(txt)
else:
- x, y = mb.board.move_to_xy(move)
+ x, y = self.move_to_xy(move)
- x, y = xa2 * 2 + xb2 * x + xc2 * y, ya2 * 2 + yb2 * x + yc2 * y
+ x, y = xa2 * (self.BOARD_SIZE - 1) + xb2 * x + xc2 * y, ya2 * (self.BOARD_SIZE - 1) + yb2 * x + yc2 * y
- boardtexts[txt] = mb.board.xy_to_move(x, y)
+ boardtexts[txt] = self.xy_to_move(x, y)
return boardtexts
ListBoard.calc_same_boardtexts = calc_same_boardtexts
次に、下記のプログラムで上記の ListBoard クラスの calc_same_boardtexts メソッド と util.py の calc_same_boardtexts が 同じ計算結果が得られることを確認 します。
8、9 行目の 座標を表す実引数を記述しない場合の返り値 である set も、10、11 行目の 座標を表す実引数を記述する場合の返り値である dict も == 演算子 で 同じ要素を持つことを判定 できます。9、11 行目の表示がいずれも True となることから、同じ計算結果が得られるが行われることが確認できました。
1 from pprint import pprint
2 from util import calc_same_boardtexts
3
4 mb = Marubatsu()
5 mb.cmove(1, 1)
6 mb.cmove(0, 0)
7 mb.cmove(1, 0)
8 pprint(mb.board.calc_same_boardtexts())
9 print(mb.board.calc_same_boardtexts() == calc_same_boardtexts(mb))
10 pprint(mb.board.calc_same_boardtexts(mb.last_move))
11 print(mb.board.calc_same_boardtexts(mb.last_move) == calc_same_boardtexts(mb, mb.last_move))
行番号のないプログラム
from pprint import pprint
from util import calc_same_boardtexts
mb = Marubatsu()
mb.cmove(1, 1)
mb.cmove(0, 0)
mb.cmove(1, 0)
pprint(mb.board.calc_same_boardtexts())
print(mb.board.calc_same_boardtexts() == calc_same_boardtexts(mb))
pprint(mb.board.calc_same_boardtexts(mb.last_move))
print(mb.board.calc_same_boardtexts(mb.last_move) == calc_same_boardtexts(mb, mb.last_move))
実行結果
{'....o..ox',
'....o.xo.',
'....oo..x',
'...oo.x..',
'..x.oo...',
'.ox.o....',
'x..oo....',
'xo..o....'}
True
{'....o..ox': (2, 1),
'....o.xo.': (2, 1),
'....oo..x': (1, 2),
'...oo.x..': (1, 0),
'..x.oo...': (1, 2),
'.ox.o....': (0, 1),
'x..oo....': (1, 0),
'xo..o....': (0, 1)}
True
また、これまでに定義した List1dBoard などの 他のデータ構造でゲーム盤を表現するクラス は ListBoard を継承してクラスを定義 しているので、List1dBoard には ListBoard と同じ計算を行う calc_same_boardtext が定義 されます。従って下記のプログラムのように List1dBoard を利用した場合でも上記と同様に calc_same_boardtext メソッドが正しい計算を行う ことが確認できます。なお、List1dBoard の座標 は 整数型の数値座標 なので、座標を実引数に記述した場合の dict のキーの値は整数 になる点が上記と異なります。
from marubatsu import List1dBoard
mb = Marubatsu(boardclass=List1dBoard)
mb.cmove(1, 1)
mb.cmove(0, 0)
mb.cmove(1, 0)
pprint(mb.board.calc_same_boardtexts())
print(mb.board.calc_same_boardtexts() == calc_same_boardtexts(mb))
pprint(mb.board.calc_same_boardtexts(mb.last_move))
print(mb.board.calc_same_boardtexts(mb.last_move) == calc_same_boardtexts(mb, mb.last_move))
実行結果
{'....o..ox',
'....o.xo.',
'....oo..x',
'...oo.x..',
'..x.oo...',
'.ox.o....',
'x..oo....',
'xo..o....'}
True
{'....o..ox': 7,
'....o.xo.': 7,
'....oo..x': 5,
'...oo.x..': 3,
'..x.oo...': 5,
'.ox.o....': 1,
'x..oo....': 3,
'xo..o....': 1}
True
本記事では行いませんが、興味がある方は ArrayBoard や NpBoard クラスを利用した場合でも同様の結果になることを確認してみて下さい。
NpBoard クラスでの定義
NpBoard クラスの calc_same_boardtexts メソッドを、下記のプログラムのように np.fliplr と np.rot90 を利用して計算 するように修正します。ListBoard クラスの calc_same_boardtexts と大きく変わったので修正箇所は省略します。
-
dataは必要がないので削除した -
6、23 行目:6 行目の後の計算で、
board属性に同一局面のゲーム盤のデータを計算して代入するので、元のゲーム盤のboard属性の値 をboardorigに代入して 取っておき、23 行目でboard属性に代入して元のゲーム盤のデータに戻す 必要がある -
9 ~ 13 行目:先程説明した方法で繰り返し処理によって同一局面を計算する。ただし、
moveに座標が代入されている場合の座標の計算の処理はこの後でまとめて行う - 14 行目:上記で計算した同一局面を表す文字列を計算する
-
15、16 行目:
moveがNoneの場合の処理を行う -
17 ~ 22 行目:
moveに座標が代入されていた場合の 同一局面に対応する座標を先程説明した方法で計算する
1 def calc_same_boardtexts(self, move=None):
2 if move is None:
3 boardtexts = set([self.board_to_str()])
4 else:
5 boardtexts = { self.board_to_str(): move }
6 boardorig = self.board
7 if move is not None:
8 x, y = self.move_to_xy(move)
9 for i in range(7):
10 if i != 3:
11 self.board = np.rot90(self.board)
12 else:
13 self.board = np.fliplr(self.board)
14 txt = self.board_to_str()
15 if move is None:
16 boardtexts.add(txt)
17 else:
18 if i == 3:
19 y = self.BOARD_SIZE - y - 1
20 else:
21 x, y = self.BOARD_SIZE - y - 1, x
22 boardtexts[txt] = self.xy_to_move(x, y)
23 self.board = boardorig
24 return boardtexts
25
26 NpBoard.calc_same_boardtexts = calc_same_boardtexts
行番号のないプログラム
def calc_same_boardtexts(self, move=None):
if move is None:
boardtexts = set([self.board_to_str()])
else:
boardtexts = { self.board_to_str(): move }
boardorig = self.board
if move is not None:
x, y = self.move_to_xy(move)
for i in range(7):
if i != 3:
self.board = np.rot90(self.board)
else:
self.board = np.fliplr(self.board)
txt = self.board_to_str()
if move is None:
boardtexts.add(txt)
else:
if i == 3:
y = self.BOARD_SIZE - y - 1
else:
x, y = self.BOARD_SIZE - y - 1, x
boardtexts[txt] = self.xy_to_move(x, y)
self.board = boardorig
return boardtexts
NpBoard.calc_same_boardtexts = calc_same_boardtexts
上記の修正後に、下記のプログラムで NpBoard クラスの calc_same_boardtexts メソッド と util.py の calc_same_boardtexts が 同じ計算結果が得られることを確認 します。
なお、上記で NpBoard クラスの calc_same_boardtexts メソッドを定義するために calc_same_boardtexts という名前の関数を定義した ので、下記のプログラムの 1 行目で util.py の calc_same_boardtexts をもう一度インポートする必要 があります。
from util import calc_same_boardtexts
mb = Marubatsu(boardclass=NpBoard)
mb.cmove(1, 1)
mb.cmove(0, 0)
mb.cmove(1, 0)
pprint(mb.board.calc_same_boardtexts())
print(mb.board.calc_same_boardtexts() == calc_same_boardtexts(mb))
pprint(mb.board.calc_same_boardtexts(mb.last_move))
print(mb.board.calc_same_boardtexts(mb.last_move) == calc_same_boardtexts(mb, mb.last_move))
実行結果
{'....o..ox',
'....o.xo.',
'....oo..x',
'...oo.x..',
'..x.oo...',
'.ox.o....',
'x..oo....',
'xo..o....'}
True
{'....o..ox': (2, 1),
'....o.xo.': (2, 1),
'....oo..x': (1, 2),
'...oo.x..': (1, 0),
'..x.oo...': (1, 2),
'.ox.o....': (0, 1),
'x..oo....': (1, 0),
'xo..o....': (0, 1)}
True
処理速度の検証
calc_same_boardtexts は ai_abs_dls では利用 されますが ai2 と ai14s では利用されない ので、これまでのベンチマークで行っていた ai2 VS ai2 と ai14s VS ai2 の対戦の 処理速度には影響を及ぼしません。そこで処理速度の検証は benchmark を利用せず に ai_abs_dls の処理の処理速度だけを検証 することにします。
ai_abs_dls の修正
ai_abs_dls はそのプログラムの中の from util import calc_same_boardtexts によって util.py の calc_same_boardtexts を呼び出しているので、下記のプログラムのように ゲーム盤を表すクラスの calc_same_boardtexts メソッドを呼び出す ように修正します。
- 9 行目の上にあった
from util import calc_same_boardtextsを削除した -
10 行目:
calc_same_boardtextsをmb.orig.board.calc_same_boardtextsに修正した
1 from ai import ai_by_mmscore, dprint
2
3 @ai_by_mmscore
4 def ai_abs_dls(mb, debug=False, timelimit_pc=None, maxdepth=1,
5 eval_func=None, eval_params={}, use_tt=False,
6 tt=None, tt_for_mo=None):
7 count = 0
8 def ab_search(mborig, depth, tt, alpha=float("-inf"), beta=float("inf")):
元と同じなので省略
9 if use_tt:
10 boardtxtlist = mborig.board.calc_same_boardtexts(bestmove)
元と同じなので省略
行番号のないプログラム
from ai import ai_by_mmscore, dprint
@ai_by_mmscore
def ai_abs_dls(mb, debug=False, timelimit_pc=None, maxdepth=1,
eval_func=None, eval_params={}, use_tt=False,
tt=None, tt_for_mo=None):
count = 0
def ab_search(mborig, depth, tt, alpha=float("-inf"), beta=float("inf")):
nonlocal count
if timelimit_pc is not None and perf_counter() >= timelimit_pc:
raise RuntimeError("time out")
count += 1
if mborig.status != Marubatsu.PLAYING or depth == maxdepth:
return eval_func(mborig, calc_score=True, **eval_params)
if use_tt:
boardtxt = mborig.board_to_str()
if boardtxt in tt:
lower_bound, upper_bound, _ = tt[boardtxt]
if lower_bound == upper_bound:
return lower_bound
elif upper_bound <= alpha:
return upper_bound
elif beta <= lower_bound:
return lower_bound
else:
alpha = max(alpha, lower_bound)
beta = min(beta, upper_bound)
else:
lower_bound = min_score
upper_bound = max_score
alphaorig = alpha
betaorig = beta
legal_moves = mborig.calc_legal_moves()
if tt_for_mo is not None:
if not use_tt:
boardtxt = mborig.board_to_str()
if boardtxt in tt_for_mo:
_, _, bestmove = tt_for_mo[boardtxt]
index = legal_moves.index(bestmove)
legal_moves[0], legal_moves[index] = legal_moves[index], legal_moves[0]
if mborig.turn == Marubatsu.CIRCLE:
score = float("-inf")
for move in legal_moves:
mborig.move(move)
abscore = ab_search(mborig, depth + 1, tt, alpha, beta)
mborig.unmove()
if abscore > score:
bestmove = move
score = max(score, abscore)
if score >= beta:
break
alpha = max(alpha, score)
else:
score = float("inf")
for move in legal_moves:
mborig.move(move)
abscore = ab_search(mborig, depth + 1, tt, alpha, beta)
mborig.unmove()
if abscore < score:
bestmove = move
score = min(score, abscore)
if score <= alpha:
break
beta = min(beta, score)
if use_tt:
boardtxtlist = mborig.board.calc_same_boardtexts(bestmove)
if score <= alphaorig:
upper_bound = score
elif score < betaorig:
lower_bound = score
upper_bound = score
else:
lower_bound = score
for boardtxt, move in boardtxtlist.items():
tt[boardtxt] = (lower_bound, upper_bound, move)
return score
min_score = float("-inf")
max_score = float("inf")
if tt is None:
tt = {}
score = ab_search(mb, depth=0, tt=tt, alpha=min_score, beta=max_score)
dprint(debug, "count =", count)
return score, count
修正箇所
from ai import ai_by_mmscore, dprint
@ai_by_mmscore
def ai_abs_dls(mb, debug=False, timelimit_pc=None, maxdepth=1,
eval_func=None, eval_params={}, use_tt=False,
tt=None, tt_for_mo=None):
count = 0
def ab_search(mborig, depth, tt, alpha=float("-inf"), beta=float("inf")):
元と同じなので省略
- from util import calc_same_boardtexts
if use_tt:
- boardtxtlist = calc_same_boardtexts(mborig, bestmove)
+ boardtxtlist = mborig.board.calc_same_boardtexts(bestmove)
元と同じなので省略
本記事では修正しませんが、他の calc_same_boardtexts を利用する関数も同様の修正を行い、ai_new.py に反映しておくことにします。
上記の修正後に下記のプログラムで NpBoard クラスを利用した場合の ai_abs_dls の処理速度を計測 します。
from ai import ai14s
boardclass = NpBoard
eval_params = {"minimax": True}
for count_linemark in [False, True]:
print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
mb = Marubatsu(boardclass=NpBoard, count_linemark=count_linemark)
%timeit ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)
実行結果
boardclass: NpBoard, count_linemark False
49.2 ms ± 277 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
boardclass: NpBoard, count_linemark True
43.6 ms ± 1.13 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
下記は、前回の記事の NpBoard を利用した場合の ai_abs_dls のベンチマークの結果 に 上記の実行結果を加えた表 です。下段が上記の実行結果です。
count_linemarks |
処理時間 |
|---|---|
False |
34.3 ms 49.2 ms |
True |
30.4 ms 43.6 ms |
上記から、残念ながら np.rot90 と np.filp を利用した同一局面の計算によって 処理速度がかなり遅くなる ことが確認できました。
np.rot90 と np.fliplr による処理速度の補足
上記の実行結果から、np.rot90 と np.fliplr を利用することで処理速度が大幅に低下すると思った人がいるかもしれませんが、処理速度が低下する原因 は〇×ゲームの ゲーム盤が小さい ため、2 次元の ndarray の 要素の数が少ないから です。
以前の記事で説明したように ndarray の要素に対する参照と代入処理 は list と比較して低速 ですが、np.flip のような numpy で定義された ndarray のすべての要素をまとめて計算する処理 を行う関数は、要素の数が多い場合 は個々の要素を計算するよりも 高速に処理を行う ことができます。また、前回の記事で検証したように、要素の数が多い程 list と比較して 処理速度が高速になる という傾向があります。
実際に np.rot90 と np.fliplr を利用した同一局面の計算 もゲーム盤を大きくして 要素の数を増やす と NpBoard クラスの calc_same_boardtexts メソッドのほうが util.py の calc_same_boardtext よりも 処理速度が速く なるので、そのことを実際に示します。
util.py の calc_same_boardtexts の修正
先程説明したように、util.py の calc_same_boardtexts には ゲーム盤のサイズが 3 でない場合 に 正しい計算が行われない というバグがあるので、下記のプログラムのように修正する必要があります。
-
3 ~ 5 行目:
2をmb.board.BOARD_SIZE - 1に修正した
1 def calc_same_boardtexts(mb, move=None):
元と同じなので省略
2 for xa, xb, xc, ya, yb, yc, xa2, xb2, xc2, ya2, yb2, yc2 in data:
元と同じなので省略
3 txt += mb.board.getmark(xa * (mb.board.BOARD_SIZE -1) + xb * x + xc * y,
4 ya * (mb.board.BOARD_SIZE -1) + yb * x + yc * y)
元と同じなので省略
5 x, y = xa2 * (mb.board.BOARD_SIZE -1) + xb2 * x + xc2 * y, ya2 * (mb.board.BOARD_SIZE -1) + yb2 * x + yc2 * y
行番号のないプログラム
def calc_same_boardtexts(mb, move=None):
data = [ [ 0, 0, 1, 1, -1, 0, 1, 0, -1, 0, 1, 0],
[ 1, -1, 0, 1, 0, -1] * 2,
[ 1, 0, -1, 0, 1, 0, 0, 0, 1, 1, -1, 0],
[ 1, -1, 0, 0, 0, 1] * 2,
[ 0, 1, 0, 1, 0, -1] * 2,
[ 1, 0, -1, 1, -1, 0] * 2,
[ 0, 0, 1, 0, 1, 0] * 2, ]
if move is None:
boardtexts = set([mb.board_to_str()])
else:
boardtexts = { mb.board_to_str(): move }
for xa, xb, xc, ya, yb, yc, xa2, xb2, xc2, ya2, yb2, yc2 in data:
txt = ""
for x in range(mb.BOARD_SIZE):
for y in range(mb.BOARD_SIZE):
txt += mb.board.getmark(xa * (mb.board.BOARD_SIZE -1) + xb * x + xc * y,
ya * (mb.board.BOARD_SIZE -1) + yb * x + yc * y)
if move is None:
boardtexts.add(txt)
else:
x, y = mb.board.move_to_xy(move)
x, y = xa2 * (mb.board.BOARD_SIZE -1) + xb2 * x + xc2 * y, ya2 * (mb.board.BOARD_SIZE -1) + yb2 * x + yc2 * y
boardtexts[txt] = mb.board.xy_to_move(x, y)
return boardtext
修正箇所
def calc_same_boardtexts(mb, move=None):
元と同じなので省略
for xa, xb, xc, ya, yb, yc, xa2, xb2, xc2, ya2, yb2, yc2 in data:
元と同じなので省略
- txt += mb.board.getmark(xa * 2 + xb * x + xc * y,
- ya * 2 + yb * x + yc * y)
+ txt += mb.board.getmark(xa * (mb.board.BOARD_SIZE -1) + xb * x + xc * y,
+ ya * (mb.board.BOARD_SIZE -1) + yb * x + yc * y)
元と同じなので省略
- x, y = xa2 * 2 + xb2 * x + xc2 * y, ya2 * 2 + yb2 * x + yc2 * y
+ x, y = xa2 * (mb.board.BOARD_SIZE -1) + xb2 * x + xc2 * y, ya2 * (mb.board.BOARD_SIZE -1) + yb2 * x + yc2 * y
処理速度の比較
次に、下記のプログラムで ゲーム盤のサイズが 3 ~ 10 のそれぞれの場合に対して NpBoard クラスと util.py の calc_same_boardtexts の処理速度を計測 し、その グラフを表示 します。グラフの表示に関する処理については前回の記事を参照して下さい。
-
9、10 行目:
timeitで計測する処理の回数は、%timeit で処理速度を計算した際に表示される値をそのまま利用した - 14 ~ 17 行目:8 つの同一局面が異なる局面になるように一番上の行と (0, 1) のマスに着手を行い、確認のためその局面を表示した
-
18 ~ 20 行目:2 種類の
calc_same_boardtextsが同じ値を計算することを確認するための表示を行った
1 import timeit
2 import matplotlib.pyplot as plt
3 import japanize_matplotlib
4 from statistics import mean
5
6 sizelist = []
7 nptime = []
8 utiltime = []
9 number = 10000
10 repeat = 7
11 for size in range(3, 11):
12 sizelist.append(size)
13 mb = Marubatsu(boardclass=NpBoard, board_size=size)
14 for x in range(size):
15 mb.cmove(x, 0)
16 mb.cmove(0, 1)
17 print(mb)
18 bt1 = mb.board.calc_same_boardtexts()
19 bt2 = calc_same_boardtexts(mb)
20 print(bt1==bt2)
21
22 stmt = "mb.board.calc_same_boardtexts()"
23 result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
24 time = mean(result) / number * 1e6
25 nptime.append(time)
26 print(f" NpBoard の場合の処理時間: {time:3.0f} μs")
27 stmt = "calc_same_boardtexts(mb)"
28 result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
29 time = mean(result) / number * 1e6
30 utiltime.append(time)
31 print(f" util.py の場合の処理時間: {time:3.0f} μs")
32
33 plt.plot(sizelist, nptime, label="NpBoard の calc_same_boardtexts")
34 plt.plot(sizelist, utiltime, label="util.py の calc_same_boardtexts")
35 plt.xlabel("ゲーム盤のサイズ")
36 plt.ylabel("処理時間の平均(μs)")
37 plt.legend()
行番号のないプログラム
import timeit
import matplotlib.pyplot as plt
import japanize_matplotlib
from statistics import mean
sizelist = []
nptime = []
utiltime = []
number = 10000
repeat = 7
for size in range(3, 11):
sizelist.append(size)
mb = Marubatsu(boardclass=NpBoard, board_size=size)
for x in range(size):
mb.cmove(x, 0)
mb.cmove(0, 1)
print(mb)
bt1 = mb.board.calc_same_boardtexts()
bt2 = calc_same_boardtexts(mb)
print(bt1==bt2)
stmt = "mb.board.calc_same_boardtexts()"
result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
time = mean(result) / number * 1e6
nptime.append(time)
print(f" NpBoard の場合の処理時間: {time:3.0f} μs")
stmt = "calc_same_boardtexts(mb)"
result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
time = mean(result) / number * 1e6
utiltime.append(time)
print(f" util.py の場合の処理時間: {time:3.0f} μs")
plt.plot(sizelist, nptime, label="NpBoard の calc_same_boardtexts")
plt.plot(sizelist, utiltime, label="util.py の calc_same_boardtexts")
plt.xlabel("ゲーム盤のサイズ")
plt.ylabel("処理時間の平均(μs)")
plt.legend()
実行結果
Turn o
oxo
X..
...
True
NpBoard の場合の処理時間: 62 μs
util.py の場合の処理時間: 39 μs
Turn x
oxox
O...
....
....
True
NpBoard の場合の処理時間: 62 μs
util.py の場合の処理時間: 65 μs
Turn o
oxoxo
X....
.....
.....
.....
True
NpBoard の場合の処理時間: 65 μs
util.py の場合の処理時間: 99 μs
Turn x
oxoxox
O.....
......
......
......
......
True
NpBoard の場合の処理時間: 64 μs
util.py の場合の処理時間: 138 μs
Turn o
oxoxoxo
X......
.......
.......
.......
.......
.......
True
NpBoard の場合の処理時間: 66 μs
util.py の場合の処理時間: 183 μs
Turn x
oxoxoxox
O.......
........
........
........
........
........
........
True
NpBoard の場合の処理時間: 68 μs
util.py の場合の処理時間: 235 μs
Turn o
oxoxoxoxo
X........
.........
.........
.........
.........
.........
.........
.........
True
NpBoard の場合の処理時間: 72 μs
util.py の場合の処理時間: 304 μs
Turn x
oxoxoxoxox
O.........
..........
..........
..........
..........
..........
..........
..........
..........
True
NpBoard の場合の処理時間: 74 μs
util.py の場合の処理時間: 377 μs
実行結果で 処理時間の表示の前 に すべて True が表示される ことから、ゲーム盤のサイズが 3 でない場合でも両者が同じ計算を行う ことが確認できます。また、グラフから ゲーム盤のサイズが 4 以上の場合 は np.rot90 と np.fliplr を利用するほうが 処理速度が速く 、ゲーム盤の サイズが大きい程 その 差が大きくなる ことが確認できました。
このことから、残念ながら〇×ゲームでは np.rot90 と np.fliplr の利用によって処理速度が低下しましたが、ゲーム盤の サイズが 8 であるオセロ のような より大きなゲーム盤の同一局面を計算 する場合は 処理速度が速くなる ことが確認できました。
Board クラスの抽象メソッドの一覧
今回の記事で calc_same_boardtexts を Board クラスの抽象メソッドに追加したので、現時点での Board クラスの抽象メソッドの一覧 を下記に示します。
| 抽象メソッド | 処理 |
|---|---|
getmark_by_move(move) |
move のマスのマークを返す |
setmark_by_move(move, mark) |
move のマスに mark を代入する |
board_to_str() |
ゲーム盤を表す文字列を返す |
judge(last_turn, last_move, move_count) |
勝敗判定を計算して返す |
count_markpats(turn, last_turn) |
局面のマークのパターンを返す |
xy_to_move(x, y) |
(x, y) のマスのゲーム盤のクラスの座標を返す |
move_to_xy(move) |
move のマスの x, y の座標を返す |
calc_legal_moves() |
合法手の一覧を表す、ゲーム盤のクラスの座標を要素とする list を返す |
calc_same_boardtexts(move) |
同一局面の一覧と move の同一局面に対応する座標を計算する |
今回の記事のまとめ
今回の記事では ndarray でゲーム盤を表現 した場合に、回転(np.rot90)と反転(np.filplr)を利用して同一局面を計算 する方法について説明しました。この方法によってプログラムを 簡潔に分かりやすく記述 することができますが、残念ながら 〇×ゲーム の場合は ゲーム盤のサイズが小さいため処理速度が低下してしまう ことがわかりました。ただし、より大きなゲーム盤では処理速度が改善する ことが確認できたので、この手法はゲーム盤のサイズが一定以上の場合に有効であることがわかります。
本記事で入力したプログラム
| リンク | 説明 |
|---|---|
| marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
| marubatsu.py | 本記事で更新した marubatsu_new.py |
| ai.py | 本記事で更新した ai_new.py |
次回の記事