目次と前回の記事
Python のバージョンとこれまでに作成したモジュール
本記事のプログラムは Python の バージョン 3.13 で実行しています。
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
mbtest.py | テストに関する関数 |
util.py | ユーティリティ関数の定義 |
tree.py | ゲーム木に関する Node、Mbtree クラスなどの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
マスの座標を表すデータに関する問題点
これまでの記事では 2 次元の list と 1 次元の list でゲーム盤のデータ構造を表現する ListBoard と List1dBoard を定義しました。これらのクラスでは ゲーム盤のデータを board
属性に代入 しますが、ListBoard では (x, y) のマスを board[x][y]
、List1dBoard では board[y + x * self.BOARD_SIZE]
で参照する点が異なります。
別の言葉で説明すると、ListBoard クラスではゲーム盤の マスの座標 を (x, y) の 2 次元の座標で表現 するのに対し、List1dBoard ではゲーム盤の座標を以前の記事で説明した下図の 1 次元の数値座標で表現 するという点が異なります。

現状の List1dBoard クラスでは、下記のプログラムのように ゲーム盤のマスの参照と代入 を行う getmark
と setmark
の仮引数に 2 次元の座標 を表す x
と y
を代入するようにしているため、2 次元の座標 を y + x * self.BOARD_SIZE
という式で 数値座標に変換する処理 を行う必要があります。
def getmark(self, x, y):
return self.board[y + x * self.BOARD_SIZE]
def setmark(self, x, y, mark):
略
self.board[y + x * self.BOARD_SIZE] = mark
上記のプログラムを、下記のプログラムのように ListBoard で マスを参照する際に利用 する 1 次元の数値座標 を代入する move
という仮引数を持つように修正することで、座標の変換の処理を行う必要がなくなる ため、処理速度の改善が期待 できます。
def getmark(self, move):
return self.board[move]
def setmark(self, move, mark):
略
self.board[move] = mark
上記では ListBoard と List1dBoard のように 異なるデータ構造でゲーム盤を表現 する際に、座標を表すデータ構造 も 異なるデータ構造表現 したほうが 処理の効率が良く なるという例を紹介しました。そこで、今回の記事では ゲーム盤を表すデータ構造ごと に、座標を表すデータ構造を変更できる ようにプログラムを修正することにします。どのように修正すればよいかについて少し考えてみて下さい。
ゲーム盤のデータを表すクラスの修正
まず、ゲーム盤のデータを表現するクラス である ListBoard と List1dBoard クラスの修正からはじめることにします。
getmark
と setmark
の仮引数の変更
ゲーム盤を表すクラス に対して 必ず定義を行う必要がある getmark
と setmark
を Marubatsu クラスなどのプログラムで 利用するため には、getmark
と setmark
が 共通の仮引数を持つ必要 があります。上記のように List1dBoard の getmark
の定義 を def getmark(self, move):
のように変更 してしまうと、ListBoard の getmark
の定義 が def getmark(self, x, y):
であることから 仮引数が一致しなくなる という問題が発生します。この問題は setmark
メソッドでも同様 です。
そこで、getmark
と setmark
の 座標を代入する仮引数 を move
の 1 つに統一する ことにします。ListBoard クラスの場合は、move
に (x, y)
という tuple を代入 することで 2 次元の座標を 1 つの仮引数に代入 します。
下記はそのように ListBoard クラス の getmark
と setmark
を修正 するプログラムです。なお、下記の修正によって 4、10 行目 の処理の分だけ 処理時間が増えると思う人がいるかもしれませんが、後で説明するように getmark
を呼び出す前に行われていた x, y = move
という処理を省略することができるので、全体としての処理時間は大きく変化しません。
-
3、9 行目:仮引数
x
、y
をmove
に修正する -
4、10 行目:
move
には座標を表す(x, y)
という tuple が代入されるので、tuple の展開を行うことで x 座標と y 座標をx
とy
に代入する
なお、説明は省略しますが、setmark
内で間違って changedmark
という名前の変数を countmark
という名前にしていたことが判明したので changemdmark
に修正しました。
1 from marubatsu import ListBoard
2
3 def getmark(self, move):
4 x, y = move
5 return self.board[x][y]
6
7 ListBoard.getmark = getmark
8
9 def setmark(self, move, mark):
10 x, y = move
元と同じなので省略
11 self.board[x][y] = mark
12
13 ListBoard.setmark = setmark
行番号のないプログラム
from marubatsu import Marubatsu, ListBoard
def getmark(self, move):
x, y = move
return self.board[x][y]
ListBoard.getmark = getmark
def setmark(self, move, mark):
x, y = move
if self.count_linemark:
if mark != Marubatsu.EMPTY:
diff = 1
changedmark = mark
else:
diff = -1
changedmark = self.board[x][y]
self.colcount[changedmark][x] += diff
self.rowcount[changedmark][y] += diff
if x == y:
self.diacount[changedmark][0] += diff
if x + y == self.BOARD_SIZE - 1:
self.diacount[changedmark][1] += diff
self.board[x][y] = mark
ListBoard.setmark = setmark
修正箇所
from marubatsu import ListBoard
-def getmark(self, x, y):
+def getmark(self, move):
+ x, y = move
return self.board[x][y]
ListBoard.getmark = getmark
-def setmark(self, x, y, mark):
+def setmark(self, move, mark):
+ x, y = move
if self.count_linemark:
if mark != Marubatsu.EMPTY:
diff = 1
- countmark = mark
+ changedmark = mark
else:
diff = -1
- countmark = self.board[x][y]
+ changedmark = self.board[x][y]
- self.colcount[countmark][x] += diff
+ self.colcount[changedmark][x] += diff
- self.rowcount[countmark][y] += diff
+ self.rowcount[changedmark][y] += diff
if x == y:
- self.diacount[countmark][0] += diff
+ self.diacount[changedmark][0] += diff
if x + y == self.BOARD_SIZE - 1:
- self.diacount[countmark][1] += diff
+ self.diacount[changedmark][1] += diff
self.board[x][y] = mark
ListBoard.setmark = setmark
下記はそのように List1dBoard クラス の getmark
と setmark
を修正 するプログラムです。
-
3、8 行目:仮引数
x
、y
をmove
に修正する -
4、17、24 行目:
self.board
の添字にmove
を記述するように修正する -
10、11 行目:直線上のマークの数を数える場合は
x
とy
座標の値が必要であるため、残念ながら数値座標を表すmove
から x と y 座標を計算する必要があるので、以前の記事1で説明した方法でその計算を行う。直線上のマークの数を数えない場合はこの処理は不要なので、9 行目の if 文の条件式がTrue
の場合のみこの処理を行うようにした
こちらは元から changedmark
が使われているのでそれに関する修正はありません。
1 from marubatsu import List1dBoard
2
3 def getmark(self, move):
4 return self.board[move]
5
6 List1dBoard.getmark = getmark
7
8 def setmark(self, move, mark):
9 if self.count_linemark:
10 x = move // self.BOARD_SIZE
11 y = move % self.BOARD_SIZE
12 if mark != Marubatsu.EMPTY:
13 diff = 1
14 changedmark = mark
15 else:
16 diff = -1
17 changedmark = self.board[move]
18 self.colcount[changedmark][x] += diff
19 self.rowcount[changedmark][y] += diff
20 if x == y:
21 self.diacount[changedmark][0] += diff
22 if x + y == self.BOARD_SIZE - 1:
23 self.diacount[changedmark][1] += diff
24 self.board[move] = mark
25
26 List1dBoard.setmark = setmark
行番号のないプログラム
from marubatsu import List1dBoard
def getmark(self, move):
return self.board[move]
List1dBoard.getmark = getmark
def setmark(self, move, mark):
if self.count_linemark:
x = move // self.BOARD_SIZE
y = move % self.BOARD_SIZE
if mark != Marubatsu.EMPTY:
diff = 1
changedmark = mark
else:
diff = -1
changedmark = self.board[move]
self.colcount[changedmark][x] += diff
self.rowcount[changedmark][y] += diff
if x == y:
self.diacount[changedmark][0] += diff
if x + y == self.BOARD_SIZE - 1:
self.diacount[changedmark][1] += diff
self.board[move] = mark
List1dBoard.setmark = setmark
修正箇所
from marubatsu import List1dBoard
-def getmark(self, x, y):
+def getmark(self, move):
- return self.board[y + x * self.BOARD_SIZE]
+ return self.board[move]
List1dBoard.getmark = getmark
-def setmark(self, x, y, mark):
+def setmark(self, move, mark):
if self.count_linemark:
+ x = move // self.BOARD_SIZE
+ y = move % self.BOARD_SIZE
if mark != Marubatsu.EMPTY:
diff = 1
changedmark = mark
else:
diff = -1
- changedmark = self.board[y + x * self.BOARD_SIZE]
+ changedmark = self.board[move]
self.colcount[changedmark][x] += diff
self.rowcount[changedmark][y] += diff
if x == y:
self.diacount[changedmark][0] += diff
if x + y == self.BOARD_SIZE - 1:
self.diacount[changedmark][1] += diff
- self.board[y + x * self.BOARD_SIZE] = mark
+ self.board[move] = mark
List1dBoard.setmark = setmark
上記の修正後に 下記の処理 を行うプログラムを実行すると、実行結果から 正しい処理が行われることが確認 できます。
- 直線状のマークの数を数える処理を行う ListBoard クラスのインスタンスを作成し、
board
属性を表示する -
setmark
で (0, 0)、(1, 0) に 〇 と × を着手し、board
属性を表示する -
getmark
で (0, 0)、(1, 0)、(2, 0) を表示する
lb = ListBoard(count_linemark=True)
print(lb.board)
lb.setmark((0, 0), Marubatsu.CIRCLE)
lb.setmark((0, 1), Marubatsu.CROSS)
print(lb.board)
print(lb.getmark((0, 0)))
print(lb.getmark((0, 1)))
print(lb.getmark((0, 2)))
実行結果
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
[['o', 'x', '.'], ['.', '.', '.'], ['.', '.', '.']]
o
x
.
次に、上記と 同様の処理 を List1dBoard クラスのインスタンスを作成した場合で行う下記のプログラムを実行すると、実行結果から 正しい処理が行われることが確認 できます。なお、getmark
と setmark
の実引数に記述する 座標は数値座標 なので、(0, 0) のマスは 0
、(0, 1) のマスは 1
、(0, 2) のマスは 2
を記述する必要があります。
lb1d = List1dBoard(count_linemark=True)
print(lb1d.board)
lb1d.setmark(0, Marubatsu.CIRCLE)
lb1d.setmark(1, Marubatsu.CROSS)
print(lb1d.board)
print(lb1d.getmark(0))
print(lb1d.getmark(1))
print(lb1d.getmark(2))
実行結果
['.', '.', '.', '.', '.', '.', '.', '.', '.']
['o', 'x', '.', '.', '.', '.', '.', '.', '.']
o
x
.
calc_legal_moves
の定義と修正
上記のように getmark
メソッドと setmark
メソッドの 仮引数を変更 したことで、ゲーム盤を表すクラスが変わる と getmark
と setmark
呼び出す際に 実引数に記述する座標のデータが変わる ことになります。そのため、Marubatsu クラスなどで getmark
と setmark
を呼び出す処理 を、ゲーム盤を表すクラスの種類ごと に if 文などで 区別して記述する必要があるのではないか と思った人がいるかもしれません。この問題は、AI どうしの対戦を行う場合 は うまく解決することができます。その方法について少し考えてみて下さい。
AI の関数 は Marubatsu クラスの calc_legal_moves
メソッドで計算された 合法手の一覧の中から着手を選択 するという処理を行います。Marubatsu クラスの calc_legal_moves
が計算する 合法手 は、下記のプログラムの 4 行目で 常に 2 次元の座標を表す tuple が計算されるので、これらの座標は ListBoard の getmark
と setmark
で 直接利用することができます が、List1dBoard の getmark
と setmark
では 利用できません。
1 def calc_legal_moves(self):
2 if self.status != Marubatsu.PLAYING:
3 return []
4 legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
5 for x in range(self.BOARD_SIZE)
6 if self.board.getmark(x, y) == Marubatsu.EMPTY]
7 return legal_moves
この問題を解決する方法として、calc_legal_moves
を ゲーム盤のデータを表現するクラスのメソッド として定義し、その ゲーム盤のデータ構造に適したデータ構造での座標の一覧を返す ようにするという方法があります。具体的には、ListBoard クラスの calc_legal_moves
では 2 次元の座標を表す tuple を、List1dBoard クラスでは 数値座標を表す整数 を要素として持つ list を返すようにします。そのように修正することで、calc_legal_moves
が計算した 合法手の 座標をそのまま getmark
と setmark
の実引数に記述できる ようになります。その結果、AI の関数 は calc_legal_moves
が計算した座標の中から最善手を選択 するので、AI が計算した最善手 を そのまま setmark
の実引数に記述 して着手することができます。
なお、「ゲーム盤のデータ構造に適したデータ構造での座標」という表現は長いので、以後は「ゲーム盤のクラスの座標」と表記することにします。
下記は ListBoard クラスの calc_legal_moves
メソッドの定義です。下記の説明と修正箇所は Marubatsu クラスの calc_legal_moves
メソッドとの違いです。
- ListBoard クラスには
status
属性は存在しないのでstatus
属性がMarubatsu.PLAYING
でない場合の処理は削除した。削除した処理 はstatus
属性が存在する Marubatsu クラスのcalc_legal_moves
で行う ことにする -
4 行目:
self.getmark(x, y)
で (x, y) のマスのマークを参照する処理は、getmark
内でself.board[x][y]
を返すという処理なので、直接self.board[x][y]
を参照するように修正した。メソッドの呼び出し処理には非常に短いながらも時間がかかるので、このように修正したほうが処理時間が短くなることが期待できる
1 def calc_legal_moves(self):
2 legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
3 for x in range(self.BOARD_SIZE)
4 if self.board[x][y] == Marubatsu.EMPTY]
5 return legal_moves
6
7 ListBoard.calc_legal_moves = calc_legal_moves
行番号のないプログラム
def calc_legal_moves(self):
legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
for x in range(self.BOARD_SIZE)
if self.board[x][y] == Marubatsu.EMPTY]
return legal_moves
ListBoard.calc_legal_moves = calc_legal_moves
修正箇所
def calc_legal_moves(self):
- if self.status != Marubatsu.PLAYING:
- return []
legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
for x in range(self.BOARD_SIZE)
- if self.board.getmark(x, y) == Marubatsu.EMPTY]
+ if self.board[x][y] == Marubatsu.EMPTY]
return legal_moves
ListBoard.calc_legal_moves = calc_legal_moves
下記は List1dBoard クラスの calc_legal_moves
メソッドの定義で、下記の説明と修正箇所は、ListBoard クラスの calc_legal_moves
との違いです。
-
2、3 行目:数値座標は 0 ~ ゲーム盤のマスの数 - 1 までの整数なので、リスト内包表記の繰り返し処理ではその数だけの繰り返し処理を行い、3 行目では数値座標を
self.board
のインデックスに直接記述するように修正した
1 def calc_legal_moves(self):
2 legal_moves = [move for move in range(self.BOARD_SIZE ** 2)
3 if self.board[move] == Marubatsu.EMPTY]
4 return legal_moves
5
6 List1dBoard.calc_legal_moves = calc_legal_moves
行番号のないプログラム
def calc_legal_moves(self):
legal_moves = [move for move in range(self.BOARD_SIZE ** 2)
if self.board[move] == Marubatsu.EMPTY]
return legal_moves
List1dBoard.calc_legal_moves = calc_legal_moves
修正箇所
def calc_legal_moves(self):
- legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
- for x in range(self.BOARD_SIZE)
+ legal_moves = [move for move in range(self.BOARD_SIZE ** 2)
- if self.board[x][y] == Marubatsu.EMPTY]
+ if self.board[move] == Marubatsu.EMPTY]
return legal_moves
List1dBoard.calc_legal_moves = calc_legal_moves
上記の定義後に、下記のプログラムで ListBoard と List1dBoard のインスタンスに対して calc_legal_moves
を呼び出すと、実行結果から ListBoard では 2 次元の座標を表す tuple が、List1dBoard では 整数の数値座標 が計算されるようになったことが確認できます。なお、先程 (0, 0) と (0, 1) に着手を行ったので、それらの座標は表示されません。
print(lb.calc_legal_moves())
print(lb1d.calc_legal_moves())
実行結果
[(1, 0), (2, 0), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
[2, 3, 4, 5, 6, 7, 8]
ポリモーフィズムによる座標に対する展開処理の実装
座標を表すデータ構造 が、ゲーム盤を表すクラスによって異なる ようになったことで、List1dBoard の judge
メソッドが 正しく動作しなくなる という問題が発生します。
下記は先ほどの ListBoard クラスのインスタンスである lb
に対して さらに (0, 2)、(1, 0)、(1, 1) の着手 を行った、5 手目の局面に対して judge
メソッドで判定を行う プログラムです。5 手目までの着手を行った理由は、4 手目以下 の場合は List1dBoard の場合に 問題が発生する処理が行われる前 に Marubatsu.PLAYING
を返り値として返すため、正しい処理が行われてしまう ためです。この局面は 決着がついていない局面 であり、実行結果のように、ListBoard クラスの場合はエラーは発生せずに 正しい結果が表示 されます。
lb.setmark((0, 2), Marubatsu.CIRCLE)
lb.setmark((1, 0), Marubatsu.CROSS)
lb.setmark((1, 1), Marubatsu.CIRCLE)
last_turn = Marubatsu.CIRCLE
last_move = (1, 1)
move_count = 5
print(lb.judge(last_turn=last_turn, last_move=last_move, move_count=move_count))
実行結果
'playing'
Marubatsu クラスの move
と judge
メソッドで上記の処理を実行すれば、last_turn
、last_move
、move_count
の計算を行う必要がないと思った人がいるかもしれませんが、setmark
と getmark
の仮引数を変更したため、それに合わせた Marubatsu クラスの修正を行う必要があります。そのため、現状では Marubatsu クラスのインスタンスを作成して move
メソッドを呼び出すとエラーが発生します。Marubatsu クラスの修正は後で行います。
一方、同様の処理 を List1dBoard クラスのインスタンスである lb1d
に対して行う と、実行結果のように エラーが発生 します。エラーの原因について少し考えてみて下さい。
lb1d.setmark(2, Marubatsu.CIRCLE) # 2 は (0, 2) の数値座標
lb1d.setmark(3, Marubatsu.CROSS) # 3 は (1, 0) の数値座標
lb1d.setmark(4, Marubatsu.CIRCLE) # 4 は (1, 1) の数値座標
last_move = 4 # 4 は直前に着手した (1, 1) の数値座標
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)
実行結果
略
--> 194 x, y = last_move
195 if self.count_linemark:
196 if self.rowcount[player][y] == self.BOARD_SIZE or \
197 self.colcount[player][x] == self.BOARD_SIZE:
TypeError: cannot unpack non-iterable int object
エラーの原因の検証
エラーメッセージから x, y = last_move
という処理でエラーが発生したことがわかります。last_move
には 4
という数値型 の数値座標を代入したため、x, y = 4
という処理が行われる ことになります。
x, y = last_move
のような 反復可能オブジェクト(iterable)の展開(unpack)処理は、以前の記事で説明したように last_move
が 反復可能オブジェクトである必要 があります。数値型(int)の 4
は 反復可能オブジェクトではない(non-iterable)ので cannot unpack non-iterable int object というエラーが表示されます。
なお、「反復可能オブジェクトの展開処理」という表記は長いので、以後は単に「展開処理」と表記することにします。
反復可能オブジェクトの性質を持つ座標を表すクラスの定義
この問題を解決するためには、上記の x, y = lastmove
の処理を x = lastmove // self.BOARD_SIZE
と y = lastmove % self.BOARD_SIZE
に修正する必要がありますが、x, y = lastmove
のような 展開処理 は Marubatsu クラスのメソッドや AI の関数などの 様々な場所で記述されている ため、それらをすべて修正する必要がある点がかなり面倒 です。
そこで、今回の記事では ポリモーフィズム を利用することで、上記のような修正を行わずに済む方法 を紹介します。そのためには、x, y = lastmove
のような 展開処理 の際に 行われる処理を理解 する必要があります。
以前の記事で説明したように、反復可能オブジェクト は __iter__
メソッドまたは、__getitem__
が適切に定義されている必要があります。__iter__
メソッドに関してはまだ説明していませんが、以前の記事で説明したように、__getitem__
メソッドを定義 することで 添字を利用した参照 を行うことができるようになります。
x, y = lastmove
は、x = lastmove[0]
と y = lastmove[1]
という 2 つの処理を 1 つにまとめた ものです。従って、lastmove
に対して 0 と 1 の添字を記述 することで x 座標と y 座標を参照 することができれば x, y = lastmove
の処理を行うことができます。
従って、座標を表す lastmove
に対して下記の処理を行う __getitem__
2メソッドが定義 されていれば x, y = lastmove
という記述で x 座標と y 座標を計算できる ようになります。
- 添字が 0 の場合に x 座標 を返り値として返す
- 添字が 1 の場合に y 座標 を返り値として返す
- それ以外の添字 の場合は IndexError を発生 させる。この処理は、展開処理では 要素を代入する変数の数 と 要素の数が一致していない と エラーが発生する ため必要である
座標を表すデータ構造 が 共通の処理を行う __getitem__
メソッドを持つ ようにすることで、同じプログラムで共通して扱うことができるようにする のが ポリモーフィズム です。
下記は __getitem__
メソッドが定義された、数値座標 を表す Move クラスの定義 です。
-
2 ~ 4 行目:数値座標を代入する仮引数
move
と、数値座標から 2 次元の (x、y) の座標を計算する際に必要となるゲーム盤のサイズを代入する仮引数board_size
を持つ__init__
メソッドを定義し、それらを同名の属性に代入する -
6 ~ 12 行目:インデックスが 0 と 1 の場合にそれぞれ x, y 座標を計算して返し、他の値の場合は IndexError を発生させる処理を行う
__getitem__
メソッドを定義する
1 class Move:
2 def __init__(self, move, board_size):
3 self.move = move
4 self.board_size = board_size
5
6 def __getitem__(self, key):
7 if key == 0:
8 return self.move // self.board_size
9 elif key == 1:
10 return self.move % self.board_size
11 else:
12 raise IndexError
行番号のないプログラム
class Move:
def __init__(self, move, board_size):
self.move = move
self.board_size = board_size
def __getitem__(self, key):
if key == 0:
return self.move // self.board_size
elif key == 1:
return self.move % self.board_size
else:
raise IndexError
下記は先ほどの lb1d
の calc_legal_moves
で計算した 合法手の一覧 から 数値座標を取り出して Move クラスのインスタンスを作成 し、展開処理で x, y 座標を計算して表示 するプログラムです。実行結果から 正しい処理が行われた ことが確認できます。
for move in lb1d.calc_legal_moves():
x, y = Move(move=move, board_size=3)
print(f"数値座標 {move} = ({x}, {y})")
実行結果
数値座標 5 = (1, 2)
数値座標 6 = (2, 0)
数値座標 7 = (2, 1)
数値座標 8 = (2, 2)
ローカルクラスとしての Move クラスの定義
上記の Move クラス は List1dBoard クラスの メソッドでのみ利用 されるクラスなので、下記のプログラムのように List1dBoard クラスの中で定義 したほうが良いでしょう。クラスの定義内 で 定義されたクラス は ローカルクラス と呼ばれ、クラス属性 と同様に self.クラス名
や List1dBoard.クラス名
を記述して利用することができます。
class List1dBoard(ListBoard):
class Move:
def __init__(self, move, board_size):
self.move = move
self.board_size = board_size
def __getitem__(self, key):
if key == 0:
return self.move // self.board_size
elif key == 1:
return self.move % self.board_size
else:
raise IndexError
他のメソッドの定義を記述する
なお、上記のように List1dBoard の クラスの定義を記述し直す 場合は、List1dBoard クラスの すべてのメソッドの定義を記述する必要がある ので、今回の記事ではクラスのメソッドを追加、修正する場合と同様の方法で、下記のプログラムで Move クラスを List1dBoard のローカルクラスとして定義 することにします。
List1dBoard.Move = Move
なお、(x, y)
という tuple は 0 と 1 の添字で x 座標と y 座標を参照できる ので ポリモーフィズムの要件を満たしています。従って、ListBoard に対して座標を表すクラスを定義したり、calc_legal_moves
メソッドを 修正する必要はありません。
calc_legal_moves
メソッドの修正
合法手の一覧を計算 する calc_legal_moves
を下記のプログラムのように修正します。
-
2 行目:リスト内包表記の要素を
self.Move
のインスタンスを作成する処理に修正する
1 def calc_legal_moves(self):
2 legal_moves = [self.Move(move, self.BOARD_SIZE) for move in range(self.BOARD_SIZE ** 2)
3 if self.board[move] == Marubatsu.EMPTY]
4 return legal_moves
5
6 List1dBoard.calc_legal_moves = calc_legal_moves
行番号のないプログラム
def calc_legal_moves(self):
legal_moves = [self.Move(move, self.BOARD_SIZE) for move in range(self.BOARD_SIZE ** 2)
if self.board[move] == Marubatsu.EMPTY]
return legal_moves
List1dBoard.calc_legal_moves = calc_legal_moves
修正箇所
def calc_legal_moves(self):
- legal_moves = [move for move in range(self.BOARD_SIZE ** 2)
+ legal_moves = [self.Move(move, self.BOARD_SIZE) for move in range(self.BOARD_SIZE ** 2)
if self.board[move] == Marubatsu.EMPTY]
return legal_moves
List1dBoard.calc_legal_moves = calc_legal_moves
上記の修正を行うことで、下記のプログラムの実行結果のように calc_legal_moves
が計算する list の要素が List1dBoard.Move
クラスのインスタンス になり、展開処理を行うことができる ようになります。先ほどのプログラムとの違いは以下の通りです。3 行目の修正は忘れやすいので注意して下さい。
-
2 行目:
move
がList1dBoard.Move
クラスのインスタンスになったので、展開処理で x 座標と y 座標を計算するように修正した -
3 行目:数値座標 は
move
のmove
属性に代入 されているので、それを数値座標として表示するように修正した
for move in lb1d.calc_legal_moves():
x, y = move
print(f"数値座標 {move.move} = ({x}, {y})")
修正箇所
for move in lb1d.calc_legal_moves():
- x, y = Move(move=move, board_size=3)
+ x, y = move
- print(f"数値座標 {move} = ({x}, {y})")
+ print(f"数値座標 {move.move} = ({x}, {y})")
実行結果
数値座標 5 = (1, 2)
数値座標 6 = (2, 0)
数値座標 7 = (2, 1)
数値座標 8 = (2, 2)
また、上記の修正を行うことで List1dBoard クラスの 座標を表すデータ が 反復可能オブジェクトになった ので、下記のプログラムのように judge
メソッドを実行してもエラーが発生しなくなります。なお、先程と異なり、last_move
には List1dBoard.Move クラスのインスタンスを代入 する必要がある点に注意して下さい。
last_move = List1dBoard.Move(4, 3) # 4 は直前に着手した (1, 1) の数値座標, 3 はゲーム盤のサイズ
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)
修正箇所
-last_move = 4
+last_move = List1dBoard.Move(4, 3)
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)
実行結果
'playing'
getmark
と setmark
の修正
List1dBoard の 座標のデータ構造 が List1dBoard.Move クラスのインスタンスに 変化した ので、getmark
と setmark
を下記のプログラムのように修正する必要があります。
-
2、9、10 行目:数値座標は ListBoard.Move クラスの
move
属性に代入されるので、move
をmove.move
に修正する - 8 行目:x と y 座標を反復オブジェクトの展開を利用して代入するように修正する
1 def getmark(self, move):
2 return self.board[move.move]
3
4 List1dBoard.getmark = getmark
5
6 def setmark(self, move, mark):
7 if self.count_linemark:
8 x, y = move
元と同じなので省略
9 changedmark = self.board[move.move]
元と同じなので省略
10 self.board[move.move] = mark
11
12 List1dBoard.setmark = setmark
行番号のないプログラム
def getmark(self, move):
return self.board[move.move]
List1dBoard.getmark = getmark
def setmark(self, move, mark):
if self.count_linemark:
x, y = move
if mark != Marubatsu.EMPTY:
diff = 1
changedmark = mark
else:
diff = -1
changedmark = self.board[move.move]
self.colcount[changedmark][x] += diff
self.rowcount[changedmark][y] += diff
if x == y:
self.diacount[changedmark][0] += diff
if x + y == self.BOARD_SIZE - 1:
self.diacount[changedmark][1] += diff
self.board[move.move] = mark
List1dBoard.setmark = setmark
修正箇所
def getmark(self, move):
- return self.board[move]
+ return self.board[move.move]
List1dBoard.getmark = getmark
def setmark(self, move, mark):
if self.count_linemark:
- x = move // self.BOARD_SIZE
- y = move % self.BOARD_SIZE
+ x, y = move
元と同じなので省略
- changedmark = self.board[move]
+ changedmark = self.board[move.move]
元と同じなので省略
- self.board[move.move] = mark
+ self.board[move.move] = mark
List1dBoard.setmark = setmark
上記の修正後に下記のプログラムで、先程と同様の処理を行うと、先程と同じ表示が行われる ことが確認できます。なお、setmark
の実引数に記述する 数値座標 を List1dBoard.Move
クラスのインスタンスに変更 する必要がある点に注意して下さい。
lb1d = List1dBoard(count_linemark=True)
print(lb1d.board)
lb1d.setmark(List1dBoard.Move(0, 3), Marubatsu.CIRCLE)
lb1d.setmark(List1dBoard.Move(1, 3), Marubatsu.CROSS)
print(lb1d.board)
print(lb1d.getmark(List1dBoard.Move(0, 3)))
print(lb1d.getmark(List1dBoard.Move(1, 3)))
print(lb1d.getmark(List1dBoard.Move(2, 3)))
実行結果
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
[['o', 'x', '.'], ['.', '.', '.'], ['.', '.', '.']]
o
x
.
また下記のプログラムで続けて (0, 2)、(1, 0)、(1, 1) の順で着手を行った後で judge
メソッドを呼び出すと、実行結果のように 正しい判定を行うことができる ことが確認できます。
lb1d.setmark(List1dBoard.Move(2, 3), Marubatsu.CIRCLE)
lb1d.setmark(List1dBoard.Move(3, 3), Marubatsu.CROSS)
lb1d.setmark(List1dBoard.Move(4, 3), Marubatsu.CIRCLE)
last_move = List1dBoard.Move(4, 3)
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)
実行結果
'playing'
人間が着手を行う場合などへの対応
getmark
と setmark
の仮引数の修正 と、calc_legal_moves
が計算する座標のデータ構造を修正 することで、同じプログラムで AI が着手を行う場合の処理を記述できる ようになりましたが、人間が着手を行う場合 は下記のような 問題が発生 します。
- 人間が着手を行う場合 は 明らかに x と y の 2 次元で座標を設定したほうがわかりやすい。例えば List1dBoard でゲーム盤のデータ構造が表現されていた場合に、(1, 2) のマスに着手を行う際に、2 + 1 * 3 = 5 という式で数値座標を計算して指定するのは大変
- Marubatsu クラスの
play
メソッドや、座標のチェックを行うcmove
メソッドで 人間が着手を行う場合 はcalc_legal_moves
で計算された 合法手の一覧からではなく、キーボードに座標を記述したり、GUI でマスをクリックすることで着手を行うので、ゲーム盤のクラスごと に 異なるデータ構造の座標データを計算 する処理を記述する必要が生じる
また、人間が着手を行う以外 でも、例えば AI の関数 の中には下記の ai3s
の定義の 3 行目のように、特定のマスのマーク を getmark
メソッドで参照 して評価値を計算するようなものがあります。他にも ai14s
などでも同様の処理が記事術されています。
1 @ai_by_candidate
2 def ai3(mb:Marubatsu, debug:bool=False) -> list[tuple[int, int]]:
3 if mb.board.getmark(1, 1) == Marubatsu.EMPTY:
4 candidate = [(1, 1)]
5 else:
6 candidate = mb.calc_legal_moves()
7 return candidate
getmark
の仮引数を変更 したため、上記のプログラムは下記のプログラムの 3 ~ 6 行目のように、ゲーム盤を表すデータ構造ごと に 異なる座標を計算する処理を記述 する必要があります。これらの問題をうまく解決する方法について少し考えてみて下さい。
1 @ai_by_candidate
2 def ai3(mb, debug=False) -> list[tuple[int, int]]:
3 if mb.boardclass == ListBoard:
4 move = (1, 1)
5 elif mb.boardclass == List1dBoard:
6 move = List1dBoard.Move(4, 3)
7 if mb.board.getmark(move) == Marubatsu.EMPTY:
8 candidate = [move]
9 else:
10 candidate = mb.calc_legal_moves()
11 return candidate
2 次元の座標とゲーム盤のクラスの座標を扱うメソッドの分離
上記の問題を解決する方法として、x、y による 2 次元の座標 と、ゲーム盤のクラスの座標 を扱うメソッドを 別々に定義して分離する という方法が考えられます。具体的には下記のようなメソッドを定義することにします。
メソッド | 処理 |
---|---|
getmark(x, y) |
今回の記事で修正を行う前の、x, y の 2 次元の座標で 座標を指定する getmark と同じ処理を行う |
setmark(x, y, mark) |
今回の記事で修正を行う前の、x, y の 2 次元の座標で 座標を指定する setmark と同じ処理を行う |
xy_to_move(x, y) |
x, y の 2 次元の座標を、ゲーム盤のクラスの座標に 変換した値を返す |
getmark_by_move(move) |
今回の記事で修正した getmark と同じ処理を行う |
setmark_by_move(move, mark) |
今回の記事で修正した setmark と同じ処理を行う |
getmark
と setmark
を 今回の記事の修正前の処理を行うよう戻した のは、Marubatsu クラスや AI の関数などで記述されている getmark
と setmark
を呼び出す処理を変更しなくても済む ようにするためです。また、xy_to_move
を定義したのは、ゲーム盤のクラスに関わらず、同じプログラム で x, y の 2 次元の座標からゲーム盤のクラスの座標を計算 できるようするためです。その具体例は後述します。
なお、getmark
と setmark
では 2 次元の座標をゲーム盤のクラスの座標に 座標の変換を行う必要が生じる ため、その分だけ 処理速度が若干遅くなる 可能性がありますが、この 2 つのメソッドを人間が着手を行う場合や、ai3
や ai14s
などの AI の関数などで特定のマスのマークの情報が必要になる場合などの、限られた場面でしか呼び出さない ようにすることで 処理速度の低下を最小限に抑える ことにします。
下記は ListBoard クラスの上記のメソッドの定義です。getmark_by_move
と setmark_by_move
の定義は 先ほどの getmark
と setmark
の定義と同じ なので 説明は省略 します。
-
11、12 行目:ListBoard クラスの座標を表す
(x, y)
を返すようにxy_to_move
メソッドを定義する -
17、22 行目:
xy_to_move
メソッドでx
とy
に代入された座標を変換した値を実引数に記述してget_mark_by_move
とset_mark_by_move
を呼び出すように修正する
1 def getmark_by_move(self, move):
2 x, y = move
3 return self.board[x][y]
4
5 ListBoard.getmark_by_move = getmark_by_move
6
7 def setmark_by_move(self, move, mark):
元と同じなので省略
8
9 ListBoard.setmark_by_move = setmark_by_move
10
11 def xy_to_move(self, x, y):
12 return (x, y)
13
14 ListBoard.xy_to_move = xy_to_move
15
16 def getmark(self, x, y):
17 return self.getmark_by_move(self.xy_to_move(x, y))
18
19 ListBoard.getmark = getmark
20
21 def setmark(self, x, y, mark):
22 return self.setmark_by_move(self.xy_to_move(x, y), mark)
23
24 ListBoard.setmark = setmark
行番号のないプログラム
def getmark_by_move(self, move):
x, y = move
return self.board[x][y]
ListBoard.getmark_by_move = getmark_by_move
def setmark_by_move(self, move, mark):
x, y = move
if self.count_linemark:
if mark != Marubatsu.EMPTY:
diff = 1
changedmark = mark
else:
diff = -1
changedmark = self.board[x][y]
self.colcount[changedmark][x] += diff
self.rowcount[changedmark][y] += diff
if x == y:
self.diacount[changedmark][0] += diff
if x + y == self.BOARD_SIZE - 1:
self.diacount[changedmark][1] += diff
self.board[x][y] = mark
ListBoard.setmark_by_move = setmark_by_move
def xy_to_move(self, x, y):
return (x, y)
ListBoard.xy_to_move = xy_to_move
def getmark(self, x, y):
return self.getmark_by_move(self.xy_to_move(x, y))
ListBoard.getmark = getmark
def setmark(self, x, y, mark):
return self.setmark_by_move(self.xy_to_move(x, y), mark)
ListBoard.setmark = setmark
修正箇所
-def getmark(self, move):
+def getmark_by_move(self, move):
x, y = move
return self.board[x][y]
ListBoard.getmark_by_move = getmark_by_move
-def setmark(self, move, mark):
+def setmark_by_move(self, move, mark):
元と同じなので省略
ListBoard.setmark_by_move = setmark_by_move
+def xy_to_move(self, x, y):
+ return (x, y)
ListBoard.xy_to_move = xy_to_move
def getmark(self, x, y):
+ return self.getmark_by_move(self.xy_to_move(x, y))
ListBoard.getmark = getmark
def setmark(self, x, y, mark):
+ return self.setmark_by_move(self.xy_to_move(x, y), mark)
ListBoard.setmark = setmark
下記は List1dBoard クラスの上記のメソッドの定義です。getmark_by_move
と setmark_by_move
の定義の説明は ListBoard と同じ理由で省略します。
-
10、11 行目:ListBoard クラスの座標を表すデータを計算して返すように
xy_to_move
メソッドを定義する -
16、21 行目:
xy_to_move
メソッドでx
とy
に代入された座標を変換した値を実引数に記述してget_mark_by_move
とset_mark_by_move
を呼び出すように修正する。ListBoard クラスのgetmark
とsetmark
と完全に同じ 定義である
1 def getmark_by_move(self, move):
2 return self.board[move.move]
3
4 List1dBoard.getmark_by_move = getmark_by_move
5
6 def setmark_by_move(self, move, mark):
元と同じなので省略
7
8 List1dBoard.setmark_by_move = setmark_by_move
9
10 def xy_to_move(self, x, y):
11 return self.Move(y + x * self.BOARD_SIZE, self.BOARD_SIZE)
12
13 List1dBoard.xy_to_move = xy_to_move
14
15 def getmark(self, x, y):
16 return self.getmark_by_move(self.xy_to_move(x, y))
17
18 List1dBoard.getmark = getmark
19
20 def setmark(self, x, y, mark):
21 return self.setmark_by_move(self.xy_to_move(x, y), mark)
22
23 List1dBoard.setmark = setmark
行番号のないプログラム
def getmark_by_move(self, move):
return self.board[move.move]
List1dBoard.getmark_by_move = getmark_by_move
def setmark_by_move(self, move, mark):
if self.count_linemark:
x, y = move
if mark != Marubatsu.EMPTY:
diff = 1
changedmark = mark
else:
diff = -1
changedmark = self.board[move.move]
self.colcount[changedmark][x] += diff
self.rowcount[changedmark][y] += diff
if x == y:
self.diacount[changedmark][0] += diff
if x + y == self.BOARD_SIZE - 1:
self.diacount[changedmark][1] += diff
self.board[move.move] = mark
List1dBoard.setmark_by_move = setmark_by_move
def xy_to_move(self, x, y):
return self.Move(y + x * self.BOARD_SIZE, self.BOARD_SIZE)
List1dBoard.xy_to_move = xy_to_move
def getmark(self, x, y):
return self.getmark_by_move(self.xy_to_move(x, y))
List1dBoard.getmark = getmark
def setmark(self, x, y, mark):
return self.setmark_by_move(self.xy_to_move(x, y), mark)
List1dBoard.setmark = setmark
修正箇所
-def getmark(self, move):
+def getmark_by_move(self, move):
return self.board[move.move]
List1dBoard.getmark_by_move = getmark_by_move
-def setmark(self, move, mark):
+def setmark_by_move(self, move, mark):
元と同じなので省略
List1dBoard.setmark_by_move = setmark_by_move
+def xy_to_move(self, x, y):
+ return self.Move(y + x * self.BOARD_SIZE, self.BOARD_SIZE)
List1dBoard.xy_to_move = xy_to_move
def getmark(self, x, y):
+ return self.getmark_by_move(self.xy_to_move(x, y))
List1dBoard.getmark = getmark
def setmark(self, x, y, mark):
+ return self.setmark_by_move(self.xy_to_move(x, y), mark)
List1dBoard.setmark = setmark
上記の修正後によって getmark
と setmark
が 今回の記事の修正前の処理に戻った ので、下記のように x 座標と y 座標 を getmark
と setmark
の 実引数に記述して実行したプログラム で 先程と同じ実行結果 になることが確認できます。なお、last_move
には ゲーム盤のクラスの座標 のデータを代入する必要がありますが、xy_to_move
メソッドを利用することで、ListBoard と List1dBoard クラスの両方の場合で、同じプログラム で x, y の 2 次元の座標を記述 して ゲーム盤のクラスの座標データを代入 することができます。下記のプログラムの ListBoard
と List1dBoard
、lb
と lb1d
以外が同じ であることを確認して下さい。
lb = ListBoard(count_linemark=True)
print(lb.board)
lb.setmark(0, 0, Marubatsu.CIRCLE)
lb.setmark(0, 1, Marubatsu.CROSS)
print(lb.board)
print(lb.getmark(0, 0))
print(lb.getmark(0, 1))
print(lb.getmark(0, 2))
実行結果
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
[['o', 'x', '.'], ['.', '.', '.'], ['.', '.', '.']]
o
x
.
lb.setmark(0, 2, Marubatsu.CIRCLE)
lb.setmark(1, 0, Marubatsu.CROSS)
lb.setmark(1, 1, Marubatsu.CIRCLE)
last_turn = Marubatsu.CIRCLE
last_move = lb.xy_to_move(1, 1)
move_count = 5
lb.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)
実行結果
'playing'
lb1d = List1dBoard(count_linemark=True)
print(lb1d.board)
lb1d.setmark(0, 0, Marubatsu.CIRCLE)
lb1d.setmark(0, 1, Marubatsu.CROSS)
print(lb1d.board)
print(lb1d.getmark(0, 0))
print(lb1d.getmark(0, 1))
print(lb1d.getmark(0, 2))
実行結果
['.', '.', '.', '.', '.', '.', '.', '.', '.']
['o', 'x', '.', '.', '.', '.', '.', '.', '.']
o
x
.
lb1d.setmark(0, 2, Marubatsu.CIRCLE)
lb1d.setmark(1, 0, Marubatsu.CROSS)
lb1d.setmark(1, 1, Marubatsu.CIRCLE)
last_turn = Marubatsu.CIRCLE
last_move = lb1d.xy_to_move(1, 1)
move_count = 5
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)
実行結果
'playing'
Marubatsu クラスと AI の関数などの修正
ListBoard と List1dBoard の修正が完了したので、行った 下記の修正に合わせて Marubatsu クラスや AI の関数などの 修正を行う必要 があります。
- x, y の 2 次元の座標を ゲーム盤のクラスの座標で表現 するようにし、その座標でマスの参照と代入 を行う
getmark_by_move
とsetmark_by_move
メソッドを定義した - x, y の 2 次元の座標を ゲーム盤のクラスの座標に変換 する
xy_to_move
を定義した -
calc_legal_moves
の処理を ゲーム盤のクラスで行う ようにした
なお、下記の処理は ポリモーフィズムの仕組みを利用 して プログラムを変更しなくても済む ようにしたので、変更する必要はありません。
- x, y の 2 次元の座標でマスの参照と代入の処理を行う
getmark
とsetmark
メソッドを利用する処理 -
x, y = move
のような 展開処理 を記述することで、ゲーム盤のクラスの座標を x 座標と y 座標の 2 次元の座標に変換 する処理
具体的に修正する必要がある箇所は以下の通りです。
-
Marubatsu クラスの
cmove
メソッドに対してこの後で説明する修正を行う -
Marubatsu クラスの
move
メソッドに対して下記の修正を行う- 座標を代入する仮引数
x
、y
をmove
に修正する -
setmark
の呼び出しをsetmark_by_move
に修正する
- 座標を代入する仮引数
-
Marubatsu クラスの
unmove
メソッドに対して下記の修正を行う-
setmark
の呼び出しをsetmark_by_move
に修正する
-
-
Marubatsu クラスの
calc_legal_moves
メソッドに対して下記の修正を行う- ゲーム盤のクラスの
calc_legal_moves
メソッドを呼び出し、その返り値を返すように修正する
- ゲーム盤のクラスの
-
move
メソッドを呼び出す際の 座標 の実引数 を、ゲーム盤のクラスの座標 に修正する -
x 座標と y 座標に関する処理を行う必要がない場合 の
x, y = move
を削除 する -
(x, y)
という tuple で記述していた座標を、xy_to_move(x, y)
を呼び出すことで ゲーム盤のクラスの座標に修正 する
cmove
メソッドの修正
座標のチェックを伴った着手を行う cmove
メソッドは、人間が着手を選択した際に利用する ことを目的として定義したので、x 座標と y 座標で座標を指定したほうが使いやすい でしょう。そこで、仮引数 x
と y
は変更しない ことにします。
cmove
メソッドで着手を行う際の 座標のチェック は 現状では move
メソッド内で行っています が、下記の理由からその処理を cmove
メソッドで行うように修正 することにします。
-
move
メソッドの 仮引数x
、y
がmove
に変更 されたので、cmove
からmove
を呼び出す際 にcmove
の 仮引数x
,y
の 2 次元の座標をxy_to_move
を呼び出して ゲーム盤のクラスの座標に変換する必要 がある -
cmove
の 仮引数x
とy
には、play_loop
メソッド内の処理で キーボードから座標を入力した場合 は 文字列型のデータが代入 されるのでxy_to_move
を呼び出すと エラーが発生する可能性 が生じる - この問題は、
cmove
内 でplace_mark
メソッドを呼び出して 座標のチェックを行ってからxy_to_move
で座標の変換を行う ことで発生しなくなる
cmove
内で place_mark
を呼び出す ように修正すると、(x, y) の座標のチェックと着手が cmove
メソッド内で行われる ようになるので、move
メソッドの仮引数 check_coord
を廃止 し、代わりに マークを配置(place)済み であることを表す仮引数 placed
を追加 して、placed=True
を実引数に記述して move
メソッドを呼び出す ことにします。
下記はそのように cmove
メソッドを修正したプログラムです。
-
2 ~ 4 行目:
place_mark
メソッドで (x, y) のマスにself.turn
のマークを配置し、配置できた場合は 3 行目でx
とy
の座標をゲーム盤のクラス座標に変換し、placed=True
を実引数に記述してmove
メソッドを呼び出すように修正する。x
、y
には文字列が代入されている場合がある ので、3 行目では組み込み関数int
を利用して 整数型のデータに型変換を行う必要がある 点に注意する事。なお、x
、y
が整数型に変換できない場合はplace_mark
の返り値がFalse
になるので、3 行目でエラーが発生することはない
1 def cmove(self, x, y):
2 if self.place_mark(x, y, self.turn):
3 move = self.board.xy_to_move(int(x), int(y))
4 self.move(move, placed=True)
5
6 Marubatsu.cmove = cmove
行番号のないプログラム
def cmove(self, x, y):
if self.place_mark(x, y, self.turn):
move = self.board.xy_to_move(int(x), int(y))
self.move(move, placed=True)
Marubatsu.cmove = cmove
修正箇所
def cmove(self, x, y):
- self.move(x, y, check_coord=True)
+ if self.place_mark(x, y, self.turn):
+ move = self.board.xy_to_move(int(x), int(y))
+ self.move(move, placed=True)
Marubatsu.cmove = cmove
move
メソッドの修正
下記は move
メソッドを修正したプログラムです。
-
1 行目:仮引数
x
とy
をmove
に修正し、仮引数check_coord
を廃止し、デフォルト値をFalse
とする仮引数placed
を追加する -
2、3 行目:
check_coord
をplaced
に修正し、placed
がFalse
の場合の処理をsetmark(x, y, self.turn)
からsetmark_by_move(move, self.turn)
に修正する -
placed
がTrue
の場合はcmove
メソッドでマークを配置済なので、3 行目の下にあったマークを配置する処理を行っていたelse
のブロックの処理を削除する -
8 行目:
x, y
という tuple で記述していた座標をmove
に修正する。この修正によって、11、13 行目の処理でrecords
属性に記録される 座標のデータ が(x, y)
という tuple から ゲーム盤のクラスの座標のデータに変更 されたことになる
1 def move(self, move, placed=False):
2 if not placed:
3 self.board.setmark_by_move(move, self.turn)
4
5 self.last_turn = self.turn
6 self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
7 self.move_count += 1
8 self.last_move = move
9 self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
10 if len(self.records) <= self.move_count:
11 self.records.append(self.last_move)
12 else:
13 self.records[self.move_count] = self.last_move
14 self.records = self.records[0:self.move_count + 1]
15
16 Marubatsu.move = move
行番号のないプログラム
def move(self, move, placed=False):
if not placed:
self.board.setmark_by_move(move, self.turn)
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.last_move = move
self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
if len(self.records) <= self.move_count:
self.records.append(self.last_move)
else:
self.records[self.move_count] = self.last_move
self.records = self.records[0:self.move_count + 1]
Marubatsu.move = move
修正箇所
-def move(self, x, y, check_coord=False):
+def move(self, move, placed=False):
- if not placed:
+ if not check_coord:
- self.board.setmark(x, y, self.turn)
+ self.board.setmark_by_move(move, self.turn)
- else:
- if not self.place_mark(x, y, self.turn):
- return
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
- self.last_move = x, y
+ self.last_move = move
self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
if len(self.records) <= self.move_count:
self.records.append(self.last_move)
else:
self.records[self.move_count] = self.last_move
self.records = self.records[0:self.move_count + 1]
Marubatsu.move = move
上記の修正後に下記のプログラムを実行することで、実行結果から 下記の条件で move
で着手を正しく行うことができることが確認 できます。
- ゲーム盤のデータ構造として ListBoard と List1dBoard を利用した場合の処理を行う
- (0, 0)、(1, 0) の順で着手を行う
1 for boardclass in [ListBoard, List1dBoard]:
2 print(f"boardclass: {boardclass.__name__}")
3 mb = Marubatsu(boardclass=boardclass)
4 move = mb.board.xy_to_move(0, 0)
5 mb.move(move)
6 print(mb)
7 move = mb.board.xy_to_move(1, 0)
8 mb.move(move)
9 print(mb)
10 print()
行番号のないプログラム
for boardclass in [ListBoard, List1dBoard]:
print(f"boardclass: {boardclass.__name__}")
mb = Marubatsu(boardclass=boardclass)
move = mb.board.xy_to_move(0, 0)
mb.move(move)
print(mb)
move = mb.board.xy_to_move(1, 0)
mb.move(move)
print(mb)
print()
実行結果
boardclass: ListBoard
Turn x
O..
...
...
Turn o
oX.
...
...
boardclass: List1dBoard
Turn x
O..
...
...
Turn o
oX.
...
...
下記のプログラムを実行することで、実行結果から下記の条件で cmove
で着手を正しく行うことができることが確認できます。cmove
は座標のチェックを行う ので、既に着手を行ったマスの座標、ゲーム盤外の座標、文字列の座標を行っています。
- ゲーム盤のデータ構造として ListBoard と List1dBoard を利用した場合の処理を行う
- キーボードから 0,0、1,0、0,0、3,5、a,b の順で入力された場合の着手を行う
for boardclass in [ListBoard, List1dBoard]:
print(f"boardclass: {boardclass.__name__}")
mb = Marubatsu(boardclass=boardclass)
mb.cmove(0, 0)
print(mb)
mb.cmove(1, 0)
print(mb)
mb.cmove(0, 0)
print(mb)
mb.cmove(3, 5)
print(mb)
mb.cmove("a", "b")
print(mb)
print()
実行結果
boardclass: ListBoard
Turn x
O..
...
...
Turn o
oX.
...
...
( 0 , 0 ) のマスにはマークが配置済です
Turn o
oX.
...
...
( 3 , 5 ) はゲーム盤の範囲外の座標です
Turn o
oX.
...
...
整数の座標を入力して下さい
Turn o
oX.
...
...
boardclass: List1dBoard
Turn x
O..
...
...
Turn o
oX.
...
...
( 0 , 0 ) のマスにはマークが配置済です
Turn o
oX.
...
...
( 3 , 5 ) はゲーム盤の範囲外の座標です
Turn o
oX.
...
...
整数の座標を入力して下さい
Turn o
oX.
...
...
unmove
メソッドの修正
下記は unmove
メソッドを修正したプログラムです。
- 2 行目の下にあった
x, y = self.last_move
は、unmove
の処理で x 座標と y 座標は必要がない ので 削除した - その下にあった
if self.move_count == 0
という if 文は、2 行目のif self.move_count > 0:
の if 文から絶対に実行されないことに気づいたので削除した - 6、7 行目:
records
属性 に記録する着手の一覧の 座標のデータ が ゲーム盤のクラスの座標に変更 されたので、7 行目で その最後のデータをlast_move
に代入 し、8 行目でset_mark_by_move
を呼び出して 直前に着手したマークを削除 するように修正した
1 def unmove(self):
2 if self.move_count > 0:
3 self.move_count -= 1
4 self.turn, self.last_turn = self.last_turn, self.turn
5 self.status = Marubatsu.PLAYING
6 last_move = self.records.pop()
7 self.board.setmark_by_move(last_move, Marubatsu.EMPTY)
8 self.last_move = self.records[-1]
9
10 Marubatsu.unmove = unmove
行番号のないプログラム
def unmove(self):
if self.move_count > 0:
self.move_count -= 1
self.turn, self.last_turn = self.last_turn, self.turn
self.status = Marubatsu.PLAYING
last_move = self.records.pop()
self.board.setmark_by_move(last_move, Marubatsu.EMPTY)
self.last_move = self.records[-1]
Marubatsu.unmove = unmove
修正箇所
def unmove(self):
if self.move_count > 0:
- x, y = self.last_move
- if self.move_count == 0:
- self.last_move = (-1, -1)
self.move_count -= 1
self.turn, self.last_turn = self.last_turn, self.turn
self.status = Marubatsu.PLAYING
- x, y = self.records.pop()
+ last_move = self.records.pop()
- self.board.setmark(x, y, Marubatsu.EMPTY)
+ self.board.setmark_by_move(last_move, Marubatsu.EMPTY)
self.last_move = self.records[-1]
Marubatsu.unmove = unmove
上記の修正後に下記のプログラムで (0, 0)、(1, 0)、(2, 0) の順で着手 を行い、unmove
メソッドを 3 回実行 すると、実行結果から 着手が正しく取り消される ことが確認できます。
for boardclass in [ListBoard, List1dBoard]:
print(f"boardclass: {boardclass.__name__}")
mb = Marubatsu(boardclass=boardclass)
mb.cmove(0, 0)
print(mb)
mb.cmove(1, 0)
print(mb)
mb.cmove(2, 0)
print(mb)
mb.unmove()
print(mb)
mb.unmove()
print(mb)
mb.unmove()
print(mb)
print()
実行結果
boardclass: ListBoard
Turn x
O..
...
...
Turn o
oX.
...
...
Turn x
oxO
...
...
Turn o
oX.
...
...
Turn x
O..
...
...
Turn o
...
...
...
boardclass: List1dBoard
Turn x
O..
...
...
Turn o
oX.
...
...
Turn x
oxO
...
...
Turn o
oX.
...
...
Turn x
O..
...
...
Turn o
...
...
...
calc_legal_moves
の修正
下記は calc_legal_moves
を修正したプログラムで、ゲームの決着がついている場合 はこれまでどおり 空の list を、そうででない場合は ゲーム盤のクラスの calc_legal_moves
メソッドの返り値を返す ように修正します。修正箇所は省略します。
def calc_legal_moves(self):
if self.status != Marubatsu.PLAYING:
return []
return self.board.calc_legal_moves()
Marubatsu.calc_legal_moves = calc_legal_moves
play_loop
の修正
Marubatsu クラスの play_loop
メソッドでは AI が計算した着手 から x 座標と y 座標を計算 して move(x, y)
で着手を行う 処理を行っていました。
AI の関数 は calc_legal_moves
で計算した合法手の中から 1 つを選択 して 返り値として返す 処理を行うので、上記の calc_legal_moves
の修正 によって ゲーム盤のクラスの座標が返る ようになっています。従って、下記のプログラムの 4、5 行目のように AI の関数の返り値を直接 move
メソッドの実引数に記述 して呼び出すように修正する必要があります。
なお、人間がキーボードから入力した着手 の場合は x 座標と y 座標で座標を入力 するので、8 行目の cmove
の呼び出し処理を 修正する必要はありません。
1 def play_loop(self, mb_gui, params=None):
元と同じなので省略
2 # ai が着手を行うかどうかを判定する
3 if ai[index] is not None:
4 move = ai[index](self, **params[index])
5 self.move(move)
6 else:
元と同じなので省略
7 x, y = xylist
8 self.cmove(x, y)
元と同じなので省略
9
10 Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self, mb_gui, params=None):
if params is None:
params = [{}, {}]
ai = self.ai
verbose = self.verbose
gui = self.gui
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲーム盤の表示
if verbose:
if gui:
# AI どうしの対戦の場合は画面を描画しない
if ai[0] is None or ai[1] is None:
mb_gui.update_gui()
# 手番を人間が担当する場合は、play メソッドを終了する
if ai[index] is None:
return
else:
print(self)
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
move = ai[index](self, **params[index])
self.move(move)
else:
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
if coord == "exit":
print("ゲームを終了します")
return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
self.cmove(x, y)
# 決着がついたので、ゲーム盤を表示する
if verbose:
if gui:
mb_gui.update_gui()
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
def play_loop(self, mb_gui, params=None):
元と同じなので省略
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
- x, y = ai[index](self, **params[index])
+ move = ai[index](self, **params[index])
- self.move(x, y)
+ self.move(move)
else:
元と同じなので省略
x, y = xylist
self.cmove(x, y)
元と同じなので省略
Marubatsu.play_loop = play_loop
その他の修正
先程説明した、下記のまだ行っていない修正を行います。
-
move
メソッドを呼び出す際の 座標 の実引数 を、ゲーム盤のクラスの座標 に修正する -
x 座標と y 座標に関する処理を行う必要がない場合 の
x, y = move
を削除 する -
(x, y)
という tuple で記述していた座標を、xy_to_move(x, y)
を呼び出すことで ゲーム盤のクラスの座標に修正 する
1 つ目と 2 つ目の修正は下記のように行います。
合法手の一覧から着手を行う処理 では、下記のようなプログラムが記述されています。
for move in self.mb.calc_legal_moves():
x, y = move
mb.move(x, y)
また、上記と同じ処理が下記のように記述されている場合もあるようです。
for x, y in self.mb.calc_legal_moves():
mb.move(x, y)
move
の仮引数が変化 したので上記のプログラムを下記のように修正する必要があります。ただし、x
,y
の値を 後で利用する場合 は x, y = move
を 削除することはできません。具体例としては Marubatsu_GUI クラスの update_gui
があります。
for move in self.mb.calc_legal_moves():
mb.move(move)
他にも move
メソッドを呼び出す処理 はすべて ゲームの盤クラスの座標を実引に記述 するように修正する必要があります。
修正の必要があるメソッドは以下の通りです。以前の記事でも言及しましたが、ポリモーフィズム を利用する場合は、共通するメソッドの仮引数などの仕様を変更 すると、そのメソッドを利用するプログラムをすべて変更する必要 が生じるので、今回のようにどうしても必要でない場合は、なるべく共通するメソッドを後から修正しないようしたほうが良い でしょう。とはいっても、後から変更したくなることが良くあるのが悩みの種になります。
- Marubatsu クラスの
change_step
メソッド - Marubatsu_GUI クラスの
update_gui
メソッド。なお、update_gui
のx, y = move
は、その後でx
とy
の値が必要になるので削除できない点に注意すること - Node クラスの
calc_children
メソッド - Mbtree クラスの
create_tree_by_df
、create_subtree
メソッド - Mbtree_GUI クラスの
update_gui
メソッド - mbtest.py の
test_judge
- ai.py の
ai_by_score
、ai_by_mmscore
、show_progress
、ai1
、ai4
、ai5
、ai6
、ai_gt7
、ai_mmdfs
、ai_mmdfs_tt
、ai_abs
、ai_abs2
、ai_abs3
、ai_abs_tt
、ai_abs_tt2
、ai_abs_tt3
、ai_nws_3score
、ai_nws_3score2
、ai_nws_3score_tt
、ai_mmdfs_all
、ai_abs_all
、ai_scout
、ai_mtdf
、ai_abs_dls
、ai_ab_iddfs
、ai_pvs_dls
下記は上記の一部を修正したプログラムで、プログラムが長いので折りたたみ、修正箇所の説明は省略します。上記の中で 前回の記事の ベンチマークの処理に関連しない test_judge
は省略 しました。同様の理由から ai.py の関数に関しては ai_by_score
、ai_by_mmscore
、ai_abs_dls
のみを記述しました。全てを修正したプログラムは marubatsu_new.py、tree_new.py、mbtest_new.py、ai_new.py を見て下さい。
なお、修正箇所 は ほとんどが x, y
を move
に修正 するというものなので、Ctrl + H のショートカットキーで呼び出せる VSCode の置換機能 を利用して x, y を検索 し、その中で 修正する必要があるものを move に置換 すると良いでしょう。その際に 「すべて置換」ボタンをクリック すると、置換してはいけない x, y までもが置換されてしまう ので押さないように注意して下さい。
なお、修正箇所が多いため、間違いや修正漏れがあるかもしれません。それらについては、間違いが見つかり次第今後の記事で修正したいと思います。
修正したプログラム
from marubatsu import Marubatsu_GUI
from tree import Node, Mbtree, Mbtree_GUI
from functools import wraps
from ai import dprint
from random import choice
from time import perf_counter
from copy import deepcopy
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
records = self.records
self.restart()
for move in records[1:step+1]:
self.move(move)
self.records = records
Marubatsu.change_step = change_step
def update_gui(self):
def calc_status_txt(score):
if score > 0:
return "〇"
elif score == 0:
return "△"
else:
return "×"
ax = self.ax
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# リプレイ中、ゲームの決着がついていた場合は背景色を変更する
is_replay = self.mb.move_count < len(self.mb.records) - 1
if self.mb.status == Marubatsu.PLAYING:
facecolor = "lightcyan" if is_replay else "white"
else:
facecolor = "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
fontsize=7*self.size, ha="center")
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
score = self.score_table[self.mb.board_to_str()]["score"]
if self.show_status:
text += " 状況 " + calc_status_txt(score)
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
# リプレイ中の場合は "Replay" を表示する
if is_replay:
text += " Replay"
ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")
self.draw_board(ax, self.mb, lw=0.7*self.size)
if self.show_status:
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
ai, params = self.status_dropdown.value
if ai == "Auto":
index = 0 if self.mb.turn == Marubatsu.CIRCLE else 1
ai = self.mb.ai[index]
params = self.params[index]
if ai is not None:
analyze = ai(self.mb, analyze=True, **params)
score_by_move = analyze["score_by_move"]
candidate = analyze["candidate"]
for move in self.mb.calc_legal_moves():
x, y = move
mb = deepcopy(move)
mb.move(move)
score = self.score_table[mb.board_to_str()]["score"]
color = "red" if move in bestmoves else "black"
text = calc_status_txt(score)
ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
if ai is not None:
if score_by_move is not None:
color = "red" if move in candidate else "black"
ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
elif move in candidate:
ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
from tree import Node
self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
def calc_children(self, bestmoves_and_score_by_board=None):
self.children = []
for move in self.mb.calc_legal_moves():
childmb = deepcopy(self.mb)
childmb.move(move)
self.insert(Node(childmb, parent=self, depth=self.depth + 1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board))
Node.calc_children = calc_children
def create_tree_by_df(self, N):
for move in N.mb.calc_legal_moves():
mb = deepcopy(N.mb)
mb.move(move)
node = Node(mb, parent=N, depth=N.depth + 1)
N.insert(node)
self.nodelist.append(node)
self.nodelist_by_depth[node.depth].append(node)
self.nodenum += 1
self.create_tree_by_df(node)
Mbtree.create_tree_by_bf = create_tree_by_df
def create_subtree(self):
bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
depth = 0
nodelist = [self.root]
centermb = self.subtree["centermb"]
centerdepth = centermb.move_count
if centerdepth == 0:
self.centernode = self.root
records = centermb.records
maxdepth = self.subtree["maxdepth"]
while len(nodelist) > 0:
childnodelist = []
for node in nodelist:
if depth < centerdepth - 1:
childmb = deepcopy(node.mb)
move = records[depth + 1]
childmb.move(move)
childnode = Node(childmb, parent=node, depth=depth+1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board)
node.insert(childnode)
childnodelist.append(childnode)
elif depth < maxdepth:
node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)
if depth == centerdepth - 1:
for move, childnode in node.children_by_move.items():
if move == records[depth + 1]:
self.centernode = childnode
childnodelist.append(self.centernode)
else:
if childnode.mb.status == Marubatsu.PLAYING:
childnode.children.append(None)
else:
childnodelist += node.children
else:
if node.mb.status == Marubatsu.PLAYING:
childmb = deepcopy(node.mb)
board_str = node.mb.board_to_str()
move = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
childmb.move(move)
childnode = Node(childmb, parent=node, depth=depth+1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board)
node.insert(childnode)
childnodelist.append(childnode)
nodelist = childnodelist
depth += 1
selectedmb = self.subtree["selectedmb"]
self.selectednode = self.root
for move in selectedmb.records[1:selectedmb.move_count+1]:
self.selectednode = self.selectednode.children_by_move[move]
Mbtree.create_subtree = create_subtree
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(-1, self.width - 1)
self.ax.set_ylim(-1, self.height - 1)
self.ax.invert_yaxis()
self.ax.axis("off")
if self.selectednode.depth <= 4:
maxdepth = self.selectednode.depth + 1
elif self.selectednode.depth == 5:
maxdepth = 7
else:
maxdepth = 9
if self.selectednode.depth <= 6:
centermb = self.selectednode.mb
else:
centermb = Marubatsu()
for move in self.selectednode.mb.records[1:7]:
centermb.move(move)
self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
"bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
self.selectednode = self.mbtree.selectednode
self.mbtree.draw_subtree(centernode=self.mbtree.centernode, selectednode=self.selectednode,
show_bestmove=True, show_score=self.show_score,
ax=self.ax, maxdepth=maxdepth, size=self.size)
disabled = self.selectednode.parent is None
self.set_button_status(self.left_button, disabled=disabled)
disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
self.set_button_status(self.right_button, disabled=disabled)
disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
self.set_button_status(self.up_button, disabled=disabled)
disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
self.set_button_status(self.down_button, disabled=disabled)
self.set_button_color(self.score_button, value=self.show_score)
Mbtree_GUI.update_gui = update_gui
def ai_by_score(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, *args, rand=True,
analyze=False, calc_score=False, minimax=False, **kwargs):
if calc_score:
score = eval_func(mb_orig, debug, *args, **kwargs)
if minimax and mb_orig.turn == Marubatsu.CIRCLE:
score *= -1
return score
dprint(debug, "Start ai_by_score")
dprint(debug, mb_orig)
legal_moves = mb_orig.calc_legal_moves()
dprint(debug, "legal_moves", legal_moves)
best_score = float("-inf")
best_moves = []
if analyze:
score_by_move = {}
for move in legal_moves:
dprint(debug, "=" * 20)
dprint(debug, "move", move)
mb_orig.move(move)
dprint(debug, mb_orig)
score = eval_func(mb_orig, debug, *args, **kwargs)
mb_orig.unmove()
dprint(debug, "score", score, "best score", best_score)
if analyze:
score_by_move[move] = score
if best_score < score:
best_score = score
best_moves = [move]
dprint(debug, "UPDATE")
dprint(debug, " best score", best_score)
dprint(debug, " best moves", best_moves)
elif best_score == score:
best_moves.append(move)
dprint(debug, "APPEND")
dprint(debug, " best moves", best_moves)
dprint(debug, "=" * 20)
dprint(debug, "Finished")
dprint(debug, "best score", best_score)
dprint(debug, "best moves", best_moves)
if analyze:
return {
"candidate": best_moves,
"score_by_move": score_by_move,
}
elif rand:
return choice(best_moves)
else:
return best_moves[0]
return wrapper
def ai_by_mmscore(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, *args, rand=True, share_tt=True,
analyze=False, calc_score=False, **kwargs):
if calc_score:
score, count = eval_func(mb_orig, debug, *args, **kwargs)
return score
starttime = perf_counter()
dprint(debug, "Start ai_by_mmscore")
dprint(debug, mb_orig)
legal_moves = mb_orig.calc_legal_moves()
dprint(debug, "legal_moves", legal_moves)
maxnode = mb_orig.turn == Marubatsu.CIRCLE
best_score = float("-inf") if maxnode else float("inf")
best_moves = []
tt = {} if share_tt else None
totalcount = 0
if analyze:
score_by_move = {}
for move in legal_moves:
dprint(debug, "=" * 20)
dprint(debug, "move", move)
mb_orig.move(move)
dprint(debug, mb_orig)
score, count = eval_func(mb_orig, debug, tt=tt, *args, **kwargs)
mb_orig.unmove()
totalcount += count
dprint(debug, "score", score, "best score", best_score)
if analyze:
score_by_move[move] = score
if (maxnode and best_score < score) or (not maxnode and best_score > score):
best_score = score
best_moves = [move]
dprint(debug, "UPDATE")
dprint(debug, " best score", best_score)
dprint(debug, " best moves", best_moves)
elif best_score == score:
best_moves.append(move)
dprint(debug, "APPEND")
dprint(debug, " best moves", best_moves)
dprint(debug, "=" * 20)
dprint(debug, "Finished")
dprint(debug, "best score", best_score)
dprint(debug, "best moves", best_moves)
bestmove = choice(best_moves) if rand else best_moves[0]
if analyze:
if share_tt:
PV = []
mb = deepcopy(mb_orig)
while mb.status == Marubatsu.PLAYING:
PV.append(bestmove)
if mb.board.getmark_by_move(bestmove) != Marubatsu.EMPTY:
print("そのマスには着手済みです")
break
mb.move(bestmove)
boardtxt = mb.board_to_str()
if boardtxt in tt:
_, _, bestmove = tt[boardtxt]
else:
break
else:
PV = bestmove
return {
"candidate": best_moves,
"score_by_move": score_by_move,
"tt": tt,
"time": perf_counter() - starttime,
"bestmove": PV[0],
"score": best_score,
"count": totalcount,
"PV": PV,
}
else:
return bestmove
return wrapper
@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)
from util import calc_same_boardtexts
if use_tt:
boardtxtlist = calc_same_boardtexts(mborig, 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 marubatsu import Marubatsu_GUI
from tree import Node, Mbtree, Mbtree_GUI
from functools import wraps
from ai import dprint
from random import choice
from time import perf_counter
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
records = self.records
self.restart()
- for x, y in records[1:step+1]:
+ for move in records[1:step+1]:
- self.move(x, y)
+ self.move(move)
self.records = records
Marubatsu.change_step = change_step
def update_gui(self):
元と同じなので省略
for move in self.mb.calc_legal_moves():
x, y = move
mb = deepcopy(move)
- mb.move(x, y)
+ mb.move(move)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
def calc_children(self, bestmoves_and_score_by_board=None):
self.children = []
- for x, y in self.mb.calc_legal_moves():
+ for move in self.mb.calc_legal_moves():
childmb = deepcopy(self.mb)
- childmb.move(x, y)
+ childmb.move(move)
self.insert(Node(childmb, parent=self, depth=self.depth + 1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board))
Node.calc_children = calc_children
def create_tree_by_df(self, N):
- legal_moves = N.mb.calc_legal_moves()
- for x, y in legal_moves:
+ for move in N.mb.calc_legal_moves():
mb = deepcopy(N.mb)
- mb.move(x, y)
+ mb.move(move)
元と同じなので省略
Mbtree.create_tree_by_bf = create_tree_by_df
def create_subtree(self):
元と同じなので省略
for node in nodelist:
if depth < centerdepth - 1:
childmb = deepcopy(node.mb)
- x, y = records[depth + 1]
+ move = records[depth + 1]
- childmb.move(x, y)
+ childmb.move(move)
元と同じなので省略
- x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
+ move = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
- childmb.move(x, y)
+ childmb.move(move)
元と同じなので省略
Mbtree.create_subtree = create_subtree
def update_gui(self):
元と同じなので省略
if self.selectednode.depth <= 6:
centermb = self.selectednode.mb
else:
centermb = Marubatsu()
- for x, y in self.selectednode.mb.records[1:7]:
+ for move in self.selectednode.mb.records[1:7]:
- centermb.move(x, y)
+ centermb.move(move)
元と同じなので省略
Mbtree_GUI.update_gui = update_gui
def ai_by_score(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, *args, rand=True,
analyze=False, calc_score=False, minimax=False, **kwargs):
元と同じなので省略
for move in legal_moves:
dprint(debug, "=" * 20)
dprint(debug, "move", move)
- x, y = move
- mb_orig.move(x, y)
+ mb_orig.move(move)
元と同じなので省略
def ai_by_mmscore(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, *args, rand=True, share_tt=True,
analyze=False, calc_score=False, **kwargs):
元と同じなので省略
for move in legal_moves:
dprint(debug, "=" * 20)
dprint(debug, "move", move)
- x, y = move
- mb_orig.move(x, y)
+ mb_orig.move(move)
元と同じなので省略
- x, y = bestmove
- if mb.board.getmark(x, y) != Marubatsu.EMPTY:
+ if mb.board.getmark_by_move(bestmove) != Marubatsu.EMPTY:
print("そのマスには着手済みです")
break
- mb.move(x, y)
+ mb.move(bestmove)
元と同じなので省略
@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):
元と同じなので省略
if mborig.turn == Marubatsu.CIRCLE:
score = float("-inf")
- for x, y in legal_moves:
+ for move in legal_moves:
- mborig.move(x, y)
+ mborig.move(move)
元と同じなので省略
else:
score = float("inf")
- for x, y in legal_moves:
+ for move in legal_moves:
- mborig.move(x, y)
+ mborig.move(move)
元と同じなので省略
ベンチマークの実行
上記の修正後に前回の記事のベンチマークを実行することにします。ただし、util.py の benchmark
では上記で修正した ai_abs_dls
を 上記の修正が行われていない ai.py からローカルにインポート しているので、上記の修正を行った ai_new.py からインポート するように benchmark
を再定義 する必要があります。なお、ローカルにインポートする関数を修正しない場合はこのような再定義を行う必要はありません。また、ai_new.py の内容は次回以降の記事では ai.py に反映するので、util.py の benchmark
を修正する必要はありません。
- 9 行目:ai_new.py からローカルにインポートするように修正する
1 import random
2 import timeit
3 from statistics import mean, stdev
4
5 def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
6 if seed is not None:
7 random.seed(seed)
8
9 from ai_new import ai2, ai14s, ai_match, ai_abs_dls
元と同じなので省略
行番号のないプログラム
import random
import timeit
from statistics import mean, stdev
def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
if seed is not None:
random.seed(seed)
from ai_new import ai2, ai14s, ai_match, ai_abs_dls
ai_match(ai=[ai2, ai2], match_num=match_num, mbparams=mbparams)
ai_match(ai=[ai14s, ai2], match_num=match_num, mbparams=mbparams)
mb = Marubatsu(**mbparams)
eval_params = {"minimax": True}
stmt = "ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)"
print("ai_abs_dls")
result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=locals())
result = [time / number for time in result]
print(f"{mean(result) * 1000:5.1f} ms ± {stdev(result) * 1000:5.1f} ms per loop (mean ± std. dev. of {repeat} runs, {number} loops each)")
修正箇所
import random
import timeit
from statistics import mean, stdev
def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
if seed is not None:
random.seed(seed)
- from ai import ai2, ai14s, ai_match, ai_abs_dls
+ from ai_new import ai2, ai14s, ai_match, ai_abs_dls
元と同じなので省略
上記の再定義後に下記のプログラムでベンチマークを実行します。
from util import benchmark
from marubatsu import ListBoard, List1dBoard
for boardclass in [ListBoard, List1dBoard]:
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:04<00:00, 11848.56it/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:00<00:00, 829.88it/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
22.7 ms ± 1.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, 15077.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:25<00:00, 1951.29it/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
20.6 ms ± 2.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
boardclass: List1dBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:05<00:00, 9057.91it/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:13<00:00, 681.72it/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
31.7 ms ± 2.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
boardclass: List1dBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:04<00:00, 10490.45it/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:32<00:00, 1528.86it/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
30.2 ms ± 1.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
下記は 前回の記事のベンチマークの結果と上記の結果をまとめた表です。上段の数値が前回の記事の結果 で、下段の数値が上記の結果 を表します。
boardclass | count_linemark |
ai2 VS ai2
|
ai14s VS ai2
|
ai_abs_dls |
---|---|---|---|---|
ListBoard | False |
12391.16 回/秒 11848.56 回/秒 |
969.45 回/秒 829.88 回/秒 |
17.4 ms 22.7 ms |
ListBoard | True |
12340.74 回/秒 15077.96 回/秒 |
1882.32 回/秒 1951.29 回/秒 |
17.6 ms 20.6 ms |
List1dBoard | False |
11801.20 回/秒 9059.91 回/秒 |
956.38 回/秒 681.72 回/秒 |
17.6 ms 31.7 ms |
List1dBoard | True |
12300.84 回/秒 10490.45 回/秒 |
1875.35 回/秒 1528.86 回/秒 |
18.6 ms 30.2 ms |
上記の表から下記のことがわかります。
-
ai2
VSai2
とai14s
VSai2
-
ListBoard を利用し、
count_linemark
がTrue
の場合は 処理速度が速く なる - それ以外の場合 ListBoard を利用する場合は処理速度が 少しだけ、List1dBoard を利用する場合は 目に見えて遅く なる
-
ListBoard を利用し、
-
ai_abs_dls
ではいずれの場合も 処理速度が遅くなる が、List1dBoard のほうが より処理速度が遅くなる
このようなことが起きる理由と、修正方法については次回の記事で説明します。
Board クラスの修正
今回の記事の修正によって、Board クラスで定義する 抽象メソッドなどを下記の表のように修正 することにします。なお、下記の表の move
は ゲーム盤のクラスの座標 を表します。
抽象メソッド | 処理 |
---|---|
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) のマスのゲーム盤のクラスの座標を返す |
calc_legal_moves() |
合法手の一覧を表す、ゲーム盤のクラスの座標を要素とする list を返す |
また、下記のメソッドは ゲーム盤のクラスによって処理が大きく変わることはないと思われる ので、Board クラスのメソッドでその処理を定義 し、ListBoard や List1dBoard クラスなどの ゲーム盤のクラスでは定義しない ことにしました。ゲーム盤のクラスによっては より効率的な方法で getmark
や setmark
の定義を行える場合 があるかもしれません。そのような場合はそのクラスで これらのメソッドをオーバーライドして定義 すると良いでしょう。
メソッド | 処理 |
---|---|
getmark(move) |
(x, y) のマスのマークを返す |
setmark(x, y, mark) |
(x, y) のマスに mark を代入する |
今回の記事のまとめ
今回の記事では 座標を表すデータ構造 を、ゲーム盤のデータ構造に適したデータ構造で表現できる ように修正しました。ただし、その修正によって処 理速度が低下 するという問題があることが判明したので、次回の記事でその問題の修正を行うことにします。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu.py | 本記事で更新した marubatsu_new.py |
tree.py | 本記事で更新した tree_new.py |
mbtest.py | 本記事で更新した mbtest_new.py |
ai.py | 本記事で更新した ai_new.py |
次回の記事