目次と前回の記事
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 の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
Board クラスの抽象メソッドの追加
前回までの記事では、ListBoard と List1dBoard という 2 種類の 異なるデータ構造 でゲーム盤を表現する クラスを定義 し、Marubatsu クラスでその 2 つのクラスのインスタンスを 切り替えて利用できる ようにしました。これは、以前の記事で説明した 異なるデータ型を扱うクラス が 共通するメソッドを持つ ようにすることで、それらのクラスのインスタンスを 共通して扱う ことができるようにするという ポリモーフィズム よるものです。
Python のポリモーフィズム では、共通するメソッド を 抽象メソッドとして定義 した 抽象クラスを定義 し、そのクラスを 継承することで実現 するのが一般的で、ゲーム盤のデータ構造を表すクラスの場合は、下記の表の抽象メソッドが定義された Board という抽象クラスを定義しました。
| 抽象メソッド | 処理 |
|---|---|
getmark(x, y) |
(x, y) のマスのマークを返す |
setmark(x, y, mark) |
(x, y) のマスに mark を代入する |
board_to_str() |
ゲーム盤を表す文字列を返す |
上記以外でも、ゲーム盤を表すデータ構造 によって 効率の良いアルゴリズムが変化するような処理 は 抽象メソッドとして定義 する必要があります。例えば、勝敗判定を行う judge メソッドや、局面のマークのパターンを数える count_markpats メソッドなどは、ゲーム盤を表すデータ構造が変わると効率の良いアルゴリズムが変化します。
そこで、ゲーム盤のデータ構造が変わると効率の良いアルゴリズムが変わる処理を行うメソッドを Board クラスの抽象メソッド として定義し、それに従って ListBoard、List1dBoard、Marubatsu クラスの 定義を修正 することにします。
なお、以下の説明では先に ListBoard と Marubatsu クラスの修正を行い、List1dBoard クラスの修正はその後で行うことにします。
勝敗判定と直線上のマークの数を数える処理の修正
今後の記事で紹介する予定のゲーム盤を表すデータ構造では、ListBoard とは 異なるアルゴリズム で 勝敗判定を行う ので、勝敗判定を行う judge メソッド を Board クラスの 抽象メソッド とし、ListBoard クラスの メソッドとして定義 することにします。
judge メソッドでは count_linemark 属性が True の場合に 直線上のマークの数を数える ことによって勝敗判定を行いますが、直線上のマークの数を数える 効率の良いアルゴリズム はゲーム盤を表すデータ構造によって 異なる可能性 があります。そこで、直線上のマークの数を数える処理 を ListBoard クラスの メソッドで行う ように修正することにします。
具体的には下記のような修正を行います。
-
count_linemark属性を ListBoard クラスの属性に変更する - 直線上のマークの数を数えるための属性を ListBoard クラスの属性に変更する
- 直線上のマークの数を数える処理を ListBoard クラスのメソッドで行うようする
ListBoard クラスの修正
最初に ListBoard クラスの修正を行うことにします。
__init__ メソッドの修正
下記は __init__ メソッドを修正したプログラムです。行った修正は Marubatsu クラスの __init__ メソッドの 対応する処理をこちらに移動 したというものです。
-
3、5 行目:デフォルト値を
Falseとした仮引数count_linemarkを追加し、同名の属性に代入する -
7 ~ 19 行目:Marubatsu クラスの
__init__メソッドで行われていたcount_linemarkがTrueの場合に直線上のマークの数を記録する属性を初期化する処理を追加する
1 from marubatsu import Marubatsu, ListBoard
2
3 def __init__(self, board_size=3, count_linemark=False):
4 self.BOARD_SIZE = board_size
5 self.count_linemark = count_linemark
6 self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
7 if self.count_linemark:
8 self.rowcount = {
9 Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
10 Marubatsu.CROSS: [0] * self.BOARD_SIZE,
11 }
12 self.colcount = {
13 Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
14 Marubatsu.CROSS: [0] * self.BOARD_SIZE,
15 }
16 self.diacount = {
17 Marubatsu.CIRCLE: [0] * 2,
18 Marubatsu.CROSS: [0] * 2,
19 }
20
21 ListBoard.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu, ListBoard
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
if self.count_linemark:
self.rowcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.colcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.diacount = {
Marubatsu.CIRCLE: [0] * 2,
Marubatsu.CROSS: [0] * 2,
}
ListBoard.__init__ = __init__
修正箇所
from marubatsu import Marubatsu, ListBoard
-def __init__(self, board_size=3):
+def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
+ self.count_linemark = count_linemark
self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
+ if self.count_linemark:
+ self.rowcount = {
+ Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
+ Marubatsu.CROSS: [0] * self.BOARD_SIZE,
+ }
+ self.colcount = {
+ Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
+ Marubatsu.CROSS: [0] * self.BOARD_SIZE,
+ }
+ self.diacount = {
+ Marubatsu.CIRCLE: [0] * 2,
+ Marubatsu.CROSS: [0] * 2,
+ }
ListBoard.__init__ = __init__
setmark メソッドの修正
setmark メソッドでは (x, y) のマスに マークを配置 する処理と、マークを削除 する処理の 両方を行います。そのため、下記のプログラムのように マークを配置 する場合は 対応する直線上のマークの数を増やし、マークを削除 する場合は 対応する直線上のマークの数を減らす 処理を行うように修正する必要がある点に注意が必要です。
-
1、15 行目:元のプログラムでは仮引数の名前が
valueとなっていたが、markのほうがふさわしいと思ったのでそのように修正した -
2 ~ 14 行目:
count_linemark属性がTrueの場合に直線上のマークの数を変更する -
3 ~ 8 行目:直線上のマークの数の変化(difference)を表す
diffと、どのマークが配置または削除されたかを表すchangedmarkの計算を行う。なお、15 行目 で (x, y) のマスにmarkを代入する必要がある ためmarkの値を変更してはいけない ので、markとは別のchangedmarkという変数を用意した -
3 ~ 5 行目:
markがMarubatsu.EMPTYでない場合はmarkに代入されたマークを配置するのでdiffに1を、changedmarkにmarkを代入する -
6 ~ 8 行目:
markがMarubatsu.EMPTYの場合は (x, y) に配置されたマークを削除するのでdiffに-1を、changedmarkにself.board[x][y]を代入する -
9 ~ 14 行目:
diffとchangedmarkの値を利用して Marubatsu クラスのmove、unmoveメソッドと同様の方法で対応する直線上のマークの数を変更する
1 def setmark(self, x, y, mark):
2 if self.count_linemark:
3 if mark != Marubatsu.EMPTY:
4 diff = 1
5 changedmark = mark
6 else:
7 diff = -1
8 changedmark = self.board[x][y]
9 self.colcount[changedmark][x] += diff
10 self.rowcount[changedmark][y] += diff
11 if x == y:
12 self.diacount[changedmark][0] += diff
13 if x + y == self.BOARD_SIZE - 1:
14 self.diacount[changedmark][1] += diff
15 self.board[x][y] = mark
16
17 ListBoard.setmark = setmark
行番号のないプログラム
def setmark(self, x, y, mark):
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
修正箇所
-def setmark(self, x, y, value):
+def setmark(self, x, y, mark):
+ 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] = value
+ self.board[x][y] = mark
ListBoard.setmark = setmark
なお、getmark メソッドが行う処理では 直線上のマークの数は変化しない ので、getmark メソッドを 修正する必要はありません。
judge メソッドの定義
次に、ListBoard クラスに勝敗判定を行う judge メソッドを定義 します。行う処理は Marubatsu クラスの judge メソッドと同じ ですが、Marubatsu クラスの judge メソッドでは勝敗判定を行う際に Marubatsu クラスの last_turn、last_move、move_count 属性の値が必要 となるので、それらの値を ListBoard クラスの judge メソッドで参照する方法 を考える必要があります。
その方法としては下記の 2 種類の方法が考えられます。
- Marubatsu クラスの
last_turn、last_move、move_count属性を ListBoard クラスの属性に変更 する - Marubatsu クラスから ListBoard クラスの
judgeメソッドを呼び出す際に、last_turn、last_move、move_count属性の値を 実引数に記述 する
本記事では下記の理由から 後者の方法を採用 することにします。
-
直前の手番 を表す
last_turn、直前の着手 を表すlast_move、着手した回数 を表すmove_countの値はゲーム盤のデータ構造が変わっても 変化しない可能性が高い データなので、前者の方法を採用した場合は ゲーム盤のデータを表すクラスごと に それらの属性に対する同じ処理を何度も記述する必要 が生じる - Marubatsu クラスの中で それらの属性を扱うプログラムをすべて修正する必要 が生じる
下記はそのように judge メソッドを定義したプログラムで、説明と修正箇所は Marubatsu クラスの judge メソッドとの違いです。
-
1 行目:仮引数
last_turn、last_move、move_countを追加する -
2、5、6 行目:
self.move_countとself.last_turnをmove_countとlast_turnに修正する。また、この後で定義するis_winnerメソッドではlast_moveの情報が必要となるのでlast_moveを実引数に加えた -
8 行目:元のプログラムでは引き分けの判定を
is_fullというメソッドで行っていたが、その判定処理は 1 行の条件文で記述できるのでその条件文を直接記述するように修正し、is_fullメソッドの利用は廃止することにした
1 def judge(self, last_turn, last_move, move_count):
2 if move_count < self.BOARD_SIZE * 2 - 1:
3 return Marubatsu.PLAYING
4 # 直前に着手を行ったプレイヤーの勝利の判定
5 if self.is_winner(last_turn, last_move):
6 return last_turn
7 # 引き分けの判定
8 elif move_count == self.BOARD_SIZE ** 2:
9 return Marubatsu.DRAW
10 # 上記のどれでもなければ決着がついていない
11 else:
12 return Marubatsu.PLAYING
13
14 ListBoard.judge = judge
行番号のないプログラム
def judge(self, last_turn, last_move, move_count):
if move_count < self.BOARD_SIZE * 2 - 1:
return Marubatsu.PLAYING
# 直前に着手を行ったプレイヤーの勝利の判定
if self.is_winner(last_turn, last_move):
return last_turn
# 引き分けの判定
elif move_count == self.BOARD_SIZE ** 2:
return Marubatsu.DRAW
# 上記のどれでもなければ決着がついていない
else:
return Marubatsu.PLAYING
ListBoard.judge = judge
修正箇所
-def judge(self):
+def judge(self, last_turn, last_move, move_count):
- if self.move_count < self.BOARD_SIZE * 2 - 1:
+ if move_count < self.BOARD_SIZE * 2 - 1:
return Marubatsu.PLAYING
# 直前に着手を行ったプレイヤーの勝利の判定
- if self.is_winner(self.last_turn):
+ if self.is_winner(last_turn, last_move):
- return self.last_turn
+ return last_turn
# 引き分けの判定
- elif self.is_full():
+ elif move_count == self.BOARD_SIZE ** 2:
return Marubatsu.DRAW
# 上記のどれでもなければ決着がついていない
else:
return Marubatsu.PLAYING
ListBoard.judge = judge
is_winner メソッドの定義
上記の judge メソッドから呼び出される is_winner メソッドを定義する必要があるので、下記のプログラムのように定義します。Marubatsu クラスの is_winner メソッドでは self.last_move を利用 するので、その値を代入する 仮引数 last_move を追加 しました
-
1 行目:仮引数
last_moveを追加する -
2 行目:
self.last_moveをlast_moveに修正する
1 def is_winner(self, player, last_move):
2 x, y = last_move
元と同じなので省略
3
4 ListBoard.is_winner = is_winner
行番号のないプログラム
def is_winner(self, player, last_move):
x, y = last_move
if self.count_linemark:
if self.rowcount[player][y] == self.BOARD_SIZE or \
self.colcount[player][x] == self.BOARD_SIZE:
return True
# 左上から右下方向の判定
if x == y and self.diacount[player][0] == self.BOARD_SIZE:
return True
# 右上から左下方向の判定
if x + y == self.BOARD_SIZE - 1 and \
self.diacount[player][1] == self.BOARD_SIZE:
return True
else:
if self.is_same(player, coord=[0, y], dx=1, dy=0) or \
self.is_same(player, coord=[x, 0], dx=0, dy=1):
return True
# 左上から右下方向の判定
if x == y and self.is_same(player, coord=[0, 0], dx=1, dy=1):
return True
# 右上から左下方向の判定
if x + y == self.BOARD_SIZE - 1 and \
self.is_same(player, coord=[self.BOARD_SIZE - 1, 0], dx=-1, dy=1):
return True
# どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
return False
ListBoard.is_winner = is_winner
修正箇所
-def is_winner(self, player):
+def is_winner(self, player, last_move):
+ x, y = last_move
元と同じなので省略
ListBoard.is_winner = is_winner
is_same メソッドの定義
上記の is_winner メソッドから呼び出される is_same メソッドを定義する必要があるので、下記のプログラムのように定義します。
-
3 行目:Marubatsu クラスの
is_sameメソッドではself.board.getmarkと記述していたが、ListBoard クラスの場is_sameメソッドではself.getmarkと記述する必要がある
1 def is_same(self, mark, coord, dx, dy):
2 x, y = coord
3 text_list = [self.getmark(x + i * dx, y + i * dy)
4 for i in range(self.BOARD_SIZE)]
5 line_text = "".join(text_list)
6 return line_text == mark * self.BOARD_SIZE
7
8 ListBoard.is_same = is_same
行番号のないプログラム
def is_same(self, mark, coord, dx, dy):
x, y = coord
text_list = [self.getmark(x + i * dx, y + i * dy)
for i in range(self.BOARD_SIZE)]
line_text = "".join(text_list)
return line_text == mark * self.BOARD_SIZE
ListBoard.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
x, y = coord
- text_list = [self.board.getmark(x + i * dx, y + i * dy)
+ text_list = [self.getmark(x + i * dx, y + i * dy)
for i in range(self.BOARD_SIZE)]
line_text = "".join(text_list)
return line_text == mark * self.BOARD_SIZE
ListBoard.is_same = is_same
動作の確認
上記の 修正が正しいことを確認 することにします。
下記は、実引数に count_linemark=False を記述して 直線上のマークの数を数えない ListBoard クラスのインスタンスを作成し、5 手目の着手で 〇 が勝利 する (0, 0)、(1, 1)、(1, 0)、(2, 2)、(2, 0) の順で着手を行った場合 の board 属性と judge メソッドによる勝敗判定を 表示 するプログラムです。judge メソッドを呼び出す際に 必要な last_turn、last_move、move_count の値の 計算を行うため に Marubatsu クラスの move メソッドと同様の処理 を行っています。
なお、ListBoard クラスには __str__ メソッドを定義していないので print(lb) によってゲーム盤を表示することはできません。
lb = ListBoard(count_linemark=False)
movelist = [(0, 0), (1, 1), (1, 0), (2, 2), (2, 0)]
turn = Marubatsu.CIRCLE
move_count = 0
for x, y in movelist:
print(f"({x}, {y}) に {turn} を着手")
lb.setmark(x, y, turn)
move_count += 1
last_move = (x, y)
last_turn = turn
turn = Marubatsu.CROSS if turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
print(lb.board)
print(lb.judge(last_turn, last_move, move_count))
print()
実行結果
(0, 0) に o を着手
[['o', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
playing
(1, 1) に x を着手
[['o', '.', '.'], ['.', 'x', '.'], ['.', '.', '.']]
playing
(1, 0) に o を着手
[['o', '.', '.'], ['o', 'x', '.'], ['.', '.', '.']]
playing
(2, 2) に x を着手
[['o', '.', '.'], ['o', 'x', '.'], ['.', '.', 'x']]
playing
(2, 0) に o を着手
[['o', '.', '.'], ['o', 'x', '.'], ['o', '.', 'x']]
o
実行結果 から 5 手目で 〇 が勝利 することを 正しく判定できる ことが確認できました。興味がある方は × が勝利する場合や引き分けになる場合についても確認してみて下さい。
下記は 実引数に count_linemark=True を記述して 直線上のマークの数を数える 場合のプログラムで、先程の表示に加えて 各行、列、斜め方向の順 で 直線上のマークを数も表示 するようにしました。実行結果から 正しい処理が行われている ことが確認できます。
lb = ListBoard(count_linemark=True)
movelist = [(0, 0), (1, 1), (1, 0), (2, 2), (2, 0)]
turn = Marubatsu.CIRCLE
move_count = 0
for x, y in movelist:
print(f"({x}, {y}) に {turn} を着手")
lb.setmark(x, y, turn)
move_count += 1
last_move = (x, y)
last_turn = turn
turn = Marubatsu.CROSS if turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
print(lb.board)
print("行", lb.rowcount)
print("列", lb.colcount)
print("斜め", lb.diacount)
print(lb.judge(last_turn, last_move, move_count))
print()
実行結果
(0, 0) に o を着手
[['o', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
行 {'o': [1, 0, 0], 'x': [0, 0, 0]}
列 {'o': [1, 0, 0], 'x': [0, 0, 0]}
斜め {'o': [1, 0], 'x': [0, 0]}
playing
(1, 1) に x を着手
[['o', '.', '.'], ['.', 'x', '.'], ['.', '.', '.']]
行 {'o': [1, 0, 0], 'x': [0, 1, 0]}
列 {'o': [1, 0, 0], 'x': [0, 1, 0]}
斜め {'o': [1, 0], 'x': [1, 1]}
playing
(1, 0) に o を着手
[['o', '.', '.'], ['o', 'x', '.'], ['.', '.', '.']]
行 {'o': [2, 0, 0], 'x': [0, 1, 0]}
列 {'o': [1, 1, 0], 'x': [0, 1, 0]}
斜め {'o': [1, 0], 'x': [1, 1]}
playing
(2, 2) に x を着手
[['o', '.', '.'], ['o', 'x', '.'], ['.', '.', 'x']]
行 {'o': [2, 0, 0], 'x': [0, 1, 1]}
列 {'o': [1, 1, 0], 'x': [0, 1, 1]}
斜め {'o': [1, 0], 'x': [2, 1]}
playing
(2, 0) に o を着手
[['o', '.', '.'], ['o', 'x', '.'], ['o', '.', 'x']]
行 {'o': [3, 0, 0], 'x': [0, 1, 1]}
列 {'o': [1, 1, 1], 'x': [0, 1, 1]}
斜め {'o': [1, 1], 'x': [2, 1]}
o
下記は 上記の局面 に対して 同じ順番でマークを削除 する処理を行うプログラムです。なお、勝敗判定を行う judge メソッドは マークが配置されたことを前提 としており、マークを削除した場合には利用できない1ので、judge メソッドに関する処理は削除 しました。実行結果から 正しい処理が行われている ことが確認できます。
for x, y in movelist:
print(f"({x}, {y}) から {lb.getmark(x, y)} を削除")
lb.setmark(x, y, Marubatsu.EMPTY)
print(lb.board)
print("行", lb.rowcount)
print("列", lb.colcount)
print("斜め", lb.diacount)
print()
実行結果
(0, 0) から o を削除
[['.', '.', '.'], ['o', 'x', '.'], ['o', '.', 'x']]
行 {'o': [2, 0, 0], 'x': [0, 1, 1]}
列 {'o': [0, 1, 1], 'x': [0, 1, 1]}
斜め {'o': [0, 1], 'x': [2, 1]}
(1, 1) から x を削除
[['.', '.', '.'], ['o', '.', '.'], ['o', '.', 'x']]
行 {'o': [2, 0, 0], 'x': [0, 0, 1]}
列 {'o': [0, 1, 1], 'x': [0, 0, 1]}
斜め {'o': [0, 1], 'x': [1, 0]}
(1, 0) から o を削除
[['.', '.', '.'], ['.', '.', '.'], ['o', '.', 'x']]
行 {'o': [1, 0, 0], 'x': [0, 0, 1]}
列 {'o': [0, 0, 1], 'x': [0, 0, 1]}
斜め {'o': [0, 1], 'x': [1, 0]}
(2, 2) から x を削除
[['.', '.', '.'], ['.', '.', '.'], ['o', '.', '.']]
行 {'o': [1, 0, 0], 'x': [0, 0, 0]}
列 {'o': [0, 0, 1], 'x': [0, 0, 0]}
斜め {'o': [0, 1], 'x': [0, 0]}
(2, 0) から o を削除
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
行 {'o': [0, 0, 0], 'x': [0, 0, 0]}
列 {'o': [0, 0, 0], 'x': [0, 0, 0]}
斜め {'o': [0, 0], 'x': [0, 0]}
なお、test_judge を利用した 勝敗判定の確認 を行うためには Marubatsu クラスの修正が必要 なので、その後で行うことにします。
Marubatsu クラスの修正
ListBoard クラスの修正にあわせて Marubatsu クラスを修正 する必要があります。主な修正内容は count_linemark に関する処理の削除 と、勝敗判定の処理 を ListBoard クラス の judge メソッドを呼び出す ようにする点です。なお、Marubatsu クラスの is_winner、is_same、is_full メソッドは 必要がなくなったので削除 することができます。
__init__ メソッドの修正
下記は __init__ メソッドを修正したプログラムです。仮引数 count_linemark は ListBoard クラスのインスタンスを作成する際に必要となるので 残したほうが良いと思う人がいるかもしれません が、今後作成する 別のゲーム盤のデータ構造を表すクラス の __init__ メソッドには 仮引数 count_linemark が存在しない場合 や、別の仮引数が存在する 場合が考えられます。このような場合は 可変長引数 *args と **kwargs にゲーム盤のデータを作成する際に 実引数に記述するデータを代入 する必要があります。
-
1 行目:仮引数
count_linemarkを削除し、11 行目の後にあったcount_linemarkに関する処理を削除する -
1、7、8 行目:可変長引数
*argsと**kwargsを追加し、同名の属性に代入する2。argsとkwargsの値はinitialize_boardでゲーム盤のデータを表すインスタンスを作成する際に実引数に記述する
1 def __init__(self, boardclass=ListBoard, board_size=3, check_coord=True, *args, **kwargs):
2 # ゲーム盤のデータ構造を定義するクラス
3 self.boardclass = boardclass
4 # ゲーム盤の縦横のサイズ
5 self.BOARD_SIZE = board_size
6 # boardclass のパラメータ
7 self.args = args
8 self.kwargs = kwargs
9 # move と unmove メソッドで座標などのチェックを行うかどうか
10 self.check_coord = check_coord
11 # 〇×ゲーム盤を再起動するメソッドを呼び出す
12 self.restart()
13
14 Marubatsu.__init__ = __init__
行番号のないプログラム
def __init__(self, boardclass=ListBoard, board_size=3, check_coord=True, *args, **kwargs):
# ゲーム盤のデータ構造を定義するクラス
self.boardclass = boardclass
# ゲーム盤の縦横のサイズ
self.BOARD_SIZE = board_size
# boardclass のパラメータ
self.args = args
self.kwargs = kwargs
# move と unmove メソッドで座標などのチェックを行うかどうか
self.check_coord = check_coord
# 〇×ゲーム盤を再起動するメソッドを呼び出す
self.restart()
Marubatsu.__init__ = __init__
修正箇所
-def __init__(self, boardclass=ListBoard, board_size=3, check_coord=True, count_linemark=False):
+def __init__(self, boardclass=ListBoard, board_size=3, check_coord=True, *args, **kwargs):
# ゲーム盤のデータ構造を定義するクラス
self.boardclass = boardclass
# ゲーム盤の縦横のサイズ
self.BOARD_SIZE = board_size
- # 直線上のマークの数を数えるかどうか
- self.count_linemark = count_linemark
+ # boardclass のパラメータ
+ self.args = args
+ self.kwargs = kwargs
# move と unmove メソッドで座標などのチェックを行うかどうか
self.check_coord = check_coord
# 〇×ゲーム盤を再起動するメソッドを呼び出す
self.restart()
Marubatsu.__init__ = __init__
initialize_board メソッドの修正
下記は initialize_board メソッドを修正したプログラムで、2 行目で ゲーム盤を表すインスタンスを作成 する際に *self.args と **self.kwargs を実引数に記述 して 実引数の展開 を行うことで、Marubatsu クラスのインスタンスを作成 した際に 記述した実引数の一部3 が boardclass のインスタンスを作成 する際の 実引数に記述される ようになります。
def initialize_board(self):
self.board = self.boardclass(self.BOARD_SIZE, *self.args, **self.kwargs)
Marubatsu.initialize_board = initialize_board
修正箇所
def initialize_board(self):
- self.board = self.boardclass(self.BOARD_SIZE)
+ self.board = self.boardclass(self.BOARD_SIZE, *self.args, **self.kwargs)
Marubatsu.initialize_board = initialize_board
restart メソッドの修正
下記は restart メソッドを修正したプログラムで、count_linemark に関する処理を削除 するという修正を行いました。
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = [self.last_move]
Marubatsu.restart = restart
修正箇所
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = [self.last_move]
- if self.count_linemark:
- self.rowcount = {
- Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
- Marubatsu.CROSS: [0] * self.BOARD_SIZE,
- }
- self.colcount = {
- Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
- Marubatsu.CROSS: [0] * self.BOARD_SIZE,
- }
- self.diacount = {
- Marubatsu.CIRCLE: [0] * 2,
- Marubatsu.CROSS: [0] * 2,
- }
Marubatsu.restart = restart
move メソッドの修正
下記は move メソッドを修正したプログラムで、count_linemark に関する処理の削除 と、judge メソッドの呼び出しの修正 を行いました。
- 6 行目の下にあった
count_linemarkに関する処理を削除した -
7 行目:勝敗判定を、必要な実引数を記述した
self.board.judgeの呼び出しによって行うように修正した
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
3 self.last_turn = self.turn
4 self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
5 self.move_count += 1
6 self.last_move = x, y
7 self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
8 if len(self.records) <= self.move_count:
9 self.records.append(self.last_move)
10 else:
11 self.records[self.move_count] = self.last_move
12 self.records = self.records[0:self.move_count + 1]
13
14 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, 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 = x, y
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):
if self.place_mark(x, y, 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 = x, y
- if self.count_linemark:
- self.colcount[self.last_turn][x] += 1
- self.rowcount[self.last_turn][y] += 1
- if x == y:
- self.diacount[self.last_turn][0] += 1
- if x + y == self.BOARD_SIZE - 1:
- self.diacount[self.last_turn][1] += 1
- self.status = self.judge()
+ 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
unmove メソッドの修正
下記は unmove メソッドを修正したプログラムで、count_linemark に関する処理の削除 を行いました。
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()
self.remove_mark(x, y)
self.last_move = self.records[-1]
Marubatsu.unmove = unmove
修正箇所
def unmove(self):
if self.move_count > 0:
x, y = self.last_move
- if self.count_linemark:
- self.colcount[self.last_turn][x] -= 1
- self.rowcount[self.last_turn][y] -= 1
- if x == y:
- self.diacount[self.last_turn][0] -= 1
- if x + y == self.BOARD_SIZE - 1:
- self.diacount[self.last_turn][1] -= 1
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()
self.remove_mark(x, y)
self.last_move = self.records[-1]
Marubatsu.unmove = unmove
judge メソッドの修正
勝敗判定 を ゲーム盤のデータを表すクラスの judge メソッドで行う ことにしたので、Marubatsu クラスの judge メソッドを下記のプログラムのように修正する必要があります。なお、修正方法は先ほどの move メソッドでの judge メソッドの呼び出しと同じ なので省略します。
def judge(self):
return self.board.judge(self.last_turn, self.last_move, self.move_count)
Marubatsu.judge = judge
動作と処理時間の確認
上記の 修正が正しいことを確認 することにします。
下記は ai_match のキーワード引数 mbparams に Marubatsu クラスの インスタンスを作成 する際に 実引数に記述できる count_linemarks と check_coord の 4 種類の組み合わせ をそれぞれ記述して ai2s VS ai2s の対戦を 10000 回行う プログラムです。
実行結果から、いずれの場合でも 対戦成績はほぼ同じ になることが確認できます。また、1 秒あたりの対戦回数の平均 は 前回の記事 の 2438.01 回と ほぼ同じ なので、今回の記事の修正によって 処理時間がほとんど変わらない ことが確認できます。
また、以前の記事 と同様に count_linemarks の有無 では 処理時間はほとんど変わりません が、check_coord を False にして 座標のチェックを行わない場合は処理速度が若干速くなる ことが確認できました。
from ai import ai_match, ai2s
ai_match(ai=[ai2s, ai2s], match_num=5000,
mbparams={"count_linemark": False, "check_coord": True})
ai_match(ai=[ai2s, ai2s], match_num=5000,
mbparams={"count_linemark": True, "check_coord": True})
ai_match(ai=[ai2s, ai2s], match_num=5000,
mbparams={"count_linemark": False, "check_coord": False})
ai_match(ai=[ai2s, ai2s], match_num=5000,
mbparams={"count_linemark": True, "check_coord": False})
実行結果
ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2540.72it/s]
count win lose draw
o 2927 1440 633
x 1435 2915 650
total 4362 4355 1283
ratio win lose draw
o 58.5% 28.8% 12.7%
x 28.7% 58.3% 13.0%
total 43.6% 43.5% 12.8%
ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2590.08it/s]
count win lose draw
o 2962 1416 622
x 1473 2891 636
total 4435 4307 1258
ratio win lose draw
o 59.2% 28.3% 12.4%
x 29.5% 57.8% 12.7%
total 44.4% 43.1% 12.6%
ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2717.71it/s]
count win lose draw
o 2907 1427 666
x 1412 2962 626
total 4319 4389 1292
ratio win lose draw
o 58.1% 28.5% 13.3%
x 28.2% 59.2% 12.5%
total 43.2% 43.9% 12.9%
ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2780.95it/s]
count win lose draw
o 2934 1452 614
x 1394 2990 616
total 4328 4442 1230
ratio win lose draw
o 58.7% 29.0% 12.3%
x 27.9% 59.8% 12.3%
total 43.3% 44.4% 12.3%
test_judge によるテスト
judge メソッドが正しく動作するかを test_judge で確認 することにします。ただし、現状の test_judge は Marubatsu クラスの インスタンスを作成 する際の 実引数を指定できない ので、以前の記事で ai_match に対して行った修正と同様の方法で下記のプログラムように 仮引数 mbparams を追加 することにします。
-
3 行目:デフォルト値を空の dict とする仮引数
mbparamsを追加する -
8 行目:Marubatsu クラスのインスタンスを作成する際に、実引数に
**mbparamsを記述してマッピング型の展開が行われるように修正する
1 from mbtest import excel_to_xy
2
3 def test_judge(testcases=None, debug=False, mbparams={}):
元と同じなので省略
4 print("Start")
5 for winner, testdata_list in testcases.items():
6 print("test winner =", winner)
7 for testdata in testdata_list:
8 mb = Marubatsu(**mbparams)
元と同じなので省略
行番号のないプログラム
from mbtest import excel_to_xy
def test_judge(testcases=None, debug=False, mbparams={}):
if testcases is None:
testcases = {
# 決着がついていない場合のテストケース
Marubatsu.PLAYING: [
# ゲーム盤に一つもマークが配置されていない場合のテストケース
"",
# 一つだけマークが配置されていない場合のテストケース
"C3,A2,B1,B2,C2,C1,A3,B3",
"A1,A2,C3,B2,C2,C1,A3,B3",
"A1,A2,B1,B2,C2,C3,A3,B3",
"A1,C3,B1,B2,C2,C1,A3,B3",
"A1,A2,B1,C3,C2,C1,A3,B3",
"A1,A2,B1,B2,C3,C1,A3,B3",
"A1,A2,B1,B2,C2,C1,C3,B3",
"A1,A2,B1,B2,A3,C1,C2,C3",
"A1,A2,B1,B2,C2,C1,A3,B3",
],
# 〇の勝利のテストケース
Marubatsu.CIRCLE: [
"A1,A2,B1,B2,C1",
"A2,A1,B2,B1,C2",
"A3,A1,B3,B1,C3",
"A1,B1,A2,B2,A3",
"B1,A1,B2,A2,B3",
"C1,A1,C2,A2,C3",
"A1,A2,B2,A3,C3",
"A3,A1,B2,A2,C1",
# 簡易的な組み合わせ網羅の 6 のテストケース
"A1,B1,A2,B2,B3,C1,C3,C2,A3",
],
# × の勝利のテストケース
Marubatsu.CROSS: [
"A2,A1,B2,B1,A3,C1",
"A1,A2,B1,B2,A3,C2",
"A1,A3,B1,B3,A2,C3",
"B1,A1,B2,A2,C1,A3",
"A1,B1,A2,B2,C1,B3",
"A1,C1,A2,C2,B1,C3",
"A2,A1,A3,B2,B1,C3",
"A1,C1,B1,B2,A2,A3",
],
# 引き分けの場合のテストケース
Marubatsu.DRAW: [
"A1,A2,B1,B2,C2,C1,A3,B3,C3",
],
}
print("Start")
for winner, testdata_list in testcases.items():
print("test winner =", winner)
for testdata in testdata_list:
mb = Marubatsu(**mbparams)
for coord in [] if testdata == "" else testdata.split(","):
x, y = excel_to_xy(coord)
mb.move(x, y)
if debug:
print(mb)
if mb.judge() == winner:
if debug:
print("ok")
else:
print("o", end="")
else:
print()
print("====================")
print("test_judge error!")
print(mb)
print("mb.judge():", mb.judge())
print("winner: ", winner)
print("====================")
print()
print("Finished")
修正箇所
from mbtest import excel_to_xy
-def test_judge(testcases=None, debug=False):
+def test_judge(testcases=None, debug=False, mbparams={}):
元と同じなので省略
print("Start")
for winner, testdata_list in testcases.items():
print("test winner =", winner)
for testdata in testdata_list:
- mb = Marubatsu()
+ mb = Marubatsu(**mbparams)
元と同じなので省略
上記の修正後に下記のプログラムを実行すると、count_linemark が False と True の両方 の場合で judge メソッドの 勝敗判定が正しく行われたことが確認 できます。
test_judge(mbparams={"count_linemark": False})
test_judge(mbparams={"count_linemark": True})
実行結果
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished
局面のマークのパターンの数を数える処理の修正
局面の マークのパターンの数を数える count_markpats もゲーム盤のデータ構造によって 効率の良いアルゴリズムが変化する ので Board クラスの 抽象メソッド とし、ListBoard クラスの メソッドとして定義 することにします。
なお、マークのパターンを 数えるのではなく 列挙する という処理を行う enum_markpats が ai8s から呼び出されていますが、enum_markpats が行う処理は count_markpats とほぼ同じ なので 両方を抽象メソッドとするのは冗長 です。調べたところ enum_markpats は ai8s のみから呼び出されている ことが判明したので ai8s を count_markpats を利用するように修正 し、enum_markpats は削除 することにします。
ListBoard クラスの修正
まず、ListBoard クラスの修正を行います。
count_markpats メソッドの定義
下記は count_markpats メソッドの定義です。この中で呼び出している count_marks メソッドでは Marubatsu クラスの turn と last_turn 属性の値が必要 になるため、先程の judge メソッドの定義方法と同様に それらを代入する仮引数を追加 することにしました。
下記の説明と修正箇所は Marubatsu クラスの count_markpats からの修正です。
-
4 行目:仮引数
turnとlast_turnを追加した -
11 行目:
self.last_turnをlast_turnに修正した -
18、20、23、26 行目:
count_marksメソッドを呼び出す際に記述する実引数にturnとlast_turnを追加した
1 from marubatsu import Markpat
2 from collections import defaultdict
3
4 def count_markpats(self, turn, last_turn):
5 markpats = defaultdict(int)
6
7 if self.count_linemark:
8 for countdict in [self.rowcount, self.colcount, self.diacount]:
9 for circlecount, crosscount in zip(countdict[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
10 emptycount = self.BOARD_SIZE - circlecount - crosscount
11 if last_turn == Marubatsu.CIRCLE:
12 markpats[(circlecount, crosscount, emptycount)] += 1
13 else:
14 markpats[(crosscount, circlecount, emptycount)] += 1
15 else:
16 # 横方向と縦方向の判定
17 for i in range(self.BOARD_SIZE):
18 count = self.count_marks(turn, last_turn, coord=[0, i], dx=1, dy=0, datatype="tuple")
19 markpats[count] += 1
20 count = self.count_marks(turn, last_turn, coord=[i, 0], dx=0, dy=1, datatype="tuple")
21 markpats[count] += 1
22 # 左上から右下方向の判定
23 count = self.count_marks(turn, last_turn, coord=[0, 0], dx=1, dy=1, datatype="tuple")
24 markpats[count] += 1
25 # 右上から左下方向の判定
26 count = self.count_marks(turn, last_turn, coord=[2, 0], dx=-1, dy=1, datatype="tuple")
27 markpats[count] += 1
28
29 return markpats
30
31 ListBoard.count_markpats = count_markpats
行番号のないプログラム
from marubatsu import Markpat
from collections import defaultdict
def count_markpats(self, turn, last_turn):
markpats = defaultdict(int)
if self.count_linemark:
for countdict in [self.rowcount, self.colcount, self.diacount]:
for circlecount, crosscount in zip(countdict[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
emptycount = self.BOARD_SIZE - circlecount - crosscount
if last_turn == Marubatsu.CIRCLE:
markpats[(circlecount, crosscount, emptycount)] += 1
else:
markpats[(crosscount, circlecount, emptycount)] += 1
else:
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
count = self.count_marks(turn, last_turn, coord=[0, i], dx=1, dy=0, datatype="tuple")
markpats[count] += 1
count = self.count_marks(turn, last_turn, coord=[i, 0], dx=0, dy=1, datatype="tuple")
markpats[count] += 1
# 左上から右下方向の判定
count = self.count_marks(turn, last_turn, coord=[0, 0], dx=1, dy=1, datatype="tuple")
markpats[count] += 1
# 右上から左下方向の判定
count = self.count_marks(turn, last_turn, coord=[2, 0], dx=-1, dy=1, datatype="tuple")
markpats[count] += 1
return markpats
ListBoard.count_markpats = count_markpats
修正箇所
from marubatsu import Markpat
from collections import defaultdict
-def count_markpats(self):
+def count_markpats(self, turn, last_turn):
markpats = defaultdict(int)
if self.count_linemark:
for countdict in [self.rowcount, self.colcount, self.diacount]:
for circlecount, crosscount in zip(countdict[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
emptycount = self.BOARD_SIZE - circlecount - crosscount
- if self.last_turn == Marubatsu.CIRCLE:
+ if last_turn == Marubatsu.CIRCLE:
markpats[(circlecount, crosscount, emptycount)] += 1
else:
markpats[(crosscount, circlecount, emptycount)] += 1
else:
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
- count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
+ count = self.count_marks(turn, last_turn, coord=[0, i], dx=1, dy=0, datatype="tuple")
markpats[count] += 1
- count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
+ count = self.count_marks(turn, last_turn, coord=[i, 0], dx=0, dy=1, datatype="tuple")
markpats[count] += 1
# 左上から右下方向の判定
- count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
+ count = self.count_marks(turn, last_turn, coord=[0, 0], dx=1, dy=1, datatype="tuple")
markpats[count] += 1
# 右上から左下方向の判定
- count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
+ count = self.count_marks(turn, last_turn, coord=[2, 0], dx=-1, dy=1, datatype="tuple")
markpats[count] += 1
return markpats
ListBoard.count_markpats = count_markpats
count_marks メソッドの定義
count_markpats から呼び出されている count_marks を下記のプログラムのように定義します。下記の説明と修正箇所は Marubatsu クラスの count_marks からの修正です。
-
1 行目:仮引数
turnとlast_turnを追加した -
12 行目:
self.turnとself.last_turnをturnとlast_turnに修正した
1 def count_marks(self, turn, last_turn, coord, dx, dy, datatype="dict"):
2 x, y = coord
3 count = defaultdict(int)
4 for _ in range(self.BOARD_SIZE):
5 count[self.getmark(x, y)] += 1
6 x += dx
7 y += dy
8
9 if datatype == "dict":
10 return count
11 else:
12 return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])
13
14 ListBoard.count_marks = count_marks
行番号のないプログラム
def count_marks(self, turn, last_turn, coord, dx, dy, datatype="dict"):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.getmark(x, y)] += 1
x += dx
y += dy
if datatype == "dict":
return count
else:
return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])
ListBoard.count_marks = count_marks
修正箇所
-def count_marks(self, coord, dx, dy, datatype="dict"):
+def count_marks(self, turn, last_turn, coord, dx, dy, datatype="dict"):
x, y = coord
count = defaultdict(int)
for _ in range(self.BOARD_SIZE):
count[self.getmark(x, y)] += 1
x += dx
y += dy
if datatype == "dict":
return count
else:
- return Markpat(count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY])
+ return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])
ListBoard.count_marks = count_marks
Marubatsu クラスの修正
Marubatsu クラスの修正は、下記のプログラムのように count_markpats の処理で 上記で定義した count_markpats を呼び出す ように修正します。修正方法は Marubatsu クラスの judge メソッドの修正と同じなので説明と修正箇所は省略します。
def count_markpats(self):
return self.board.count_markpats(self.turn, self.last_turn)
Marubatsu.count_markpats = count_markpats
ai8s の修正
enum_markpats メソッドを廃止 したので、そのメソッドを呼び出す ai8s を修正 する必要があります。ai8s では 特定のマークのパターンが存在する かどうかで 評価値の計算 を行いますが、その判定 は count_markpats で計算した マークのパターンの数が 1 以上であるか で判定することができます。従って、ai8s は下記のプログラムのように修正できます。
-
5 行目:
enum_markpatsをcount_markpatsに修正する - 7、10 行目:指定したマークのパターンが 0 より大きいことを判定することで、そのマークのパターンが存在することを判定するように修正する
1 from ai import ai_by_score
2
3 @ai_by_score
4 def ai8s(mb, debug=False):
元と同じなので省略
5 markpats = mb.count_markpats()
6 # 相手が勝利できる場合は評価値として -1 を返す
7 if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
8 return -1
9 # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
10 elif markpats[Markpat(last_turn=2, turn=0, empty=1)] > 0:
11 return 1
12 # それ以外の場合は評価値として 0 を返す
13 else:
14 return 0
行番号のないプログラム
from ai import ai_by_score
@ai_by_score
def ai8s(mb, debug=False):
# 真ん中のマスに着手している場合は、評価値として 3 を返す
if mb.last_move == (1, 1):
return 3
# 自分が勝利している場合は、評価値として 2 を返す
if mb.status == mb.last_turn:
return 2
markpats = mb.count_markpats()
# 相手が勝利できる場合は評価値として -1 を返す
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -1
# 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] > 0:
return 1
# それ以外の場合は評価値として 0 を返す
else:
return 0
修正箇所
from ai import ai_by_score
@ai_by_score
def ai8s(mb, debug=False):
元と同じなので省略
- markpats = mb.enum_markpats()
+ markpats = mb.count_markpats()
# 相手が勝利できる場合は評価値として -1 を返す
- if Markpat(last_turn=0, turn=2, empty=1) in markpats:
+ if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -1
# 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
- elif Markpat(last_turn=2, turn=0, empty=1) in markpats:
+ elif markpats[Markpat(last_turn=2, turn=0, empty=1)] > 0:
return 1
# それ以外の場合は評価値として 0 を返す
else:
return 0
動作の確認
上記の修正後に下記のプログラムで上記で修正した ai8s と、count_markpats を利用する ai14s のそれぞれと ai2s の対戦 を行うことにします。また、その際に count_linemark が True と False のそれぞれの場合で対戦を行います。
from ai import ai14s
ai_match(ai=[ai8s, ai2s], mbparams={"count_linemark": False})
ai_match(ai=[ai8s, ai2s], mbparams={"count_linemark": True})
ai_match(ai=[ai14s, ai2s], mbparams={"count_linemark": False})
ai_match(ai=[ai14s, ai2s], mbparams={"count_linemark": True})
実行結果
ai8s VS ai2s
100%|██████████| 10000/10000 [00:10<00:00, 938.44it/s]
count win lose draw
o 9852 14 134
x 8953 226 821
total 18805 240 955
ratio win lose draw
o 98.5% 0.1% 1.3%
x 89.5% 2.3% 8.2%
total 94.0% 1.2% 4.8%
ai8s VS ai2s
100%|██████████| 10000/10000 [00:05<00:00, 1784.56it/s]
count win lose draw
o 9858 19 123
x 8856 225 919
total 18714 244 1042
ratio win lose draw
o 98.6% 0.2% 1.2%
x 88.6% 2.2% 9.2%
total 93.6% 1.2% 5.2%
ai14s VS ai2s
100%|██████████| 10000/10000 [00:11<00:00, 858.05it/s]
count win lose draw
o 9896 0 104
x 8849 0 1151
total 18745 0 1255
ratio win lose draw
o 99.0% 0.0% 1.0%
x 88.5% 0.0% 11.5%
total 93.7% 0.0% 6.3%
ai14s VS ai2s
100%|██████████| 10000/10000 [00:06<00:00, 1449.95it/s]
count win lose draw
o 9915 0 85
x 8774 0 1226
total 18689 0 1311
ratio win lose draw
o 99.2% 0.0% 0.9%
x 87.7% 0.0% 12.3%
total 93.4% 0.0% 6.6%
以前の記事と上記でで行った ai8s VS ai2s と ai14s VS ai2s の対戦成績をまとめた表です。同じ AI どうし の対戦成績はいずれも ほぼ同じ なので、先程の 修正が正しく行われたことが確認 できます。
| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 |
|---|---|---|---|---|---|---|---|---|---|
以前の ai8s |
98.2 | 0.1 | 1.6 | 89.4 | 2.5 | 8.1 | 93.8 | 1.3 | 4.9 |
count_linemark=Falseai8s
|
98.5 | 0.1 | 1.3 | 89.5 | 2.3 | 8.2 | 94.0 | 1.2 | 4.8 |
count_linemark=Trueai8s
|
98.6 | 0.2 | 1.2 | 88.6 | 2.2 | 9.2 | 93.6 | 1.2 | 5.2 |
以前の ai14s |
99.0 | 0.0 | 1.0 | 88.8 | 0.0 | 11.2 | 93.9 | 0.0 | 6.1 |
count_linemark=Falseai14s
|
99.0 | 0.0 | 1.0 | 88.5 | 0.0 | 11.5 | 93.7 | 0.0 | 6.3 |
count_linemark=Falseai14s
|
99.2 | 0.0 | 0.9 | 87.7 | 0.0 | 12.3 | 93.4 | 6.6 | 4.8 |
また、count_linemark が True の場合はいずれも 1 秒間の対戦回数の平均 が 2 倍弱ほど増えている ので、以前の記事と同様に 直線上のマークの数を数える という手法によって ai8s と ai14s の count_markpats 処理の高速化が行われる ことが確認できました。
Board クラスの修正
今回の記事で、Board クラスに 抽象メソッドとして定義するメソッドが増えた ので、下記のプログラムのように judge、count_markpats メソッドを Board クラスの抽象メソッドとして定義することにします。
from marubatsu import Board
from abc import abstractmethod
@abstractmethod
def judge(self, last_turn, last_move, move_count):
pass
Board.judge = judge
@abstractmethod
def count_markpats(self, turn, last_turn):
pass
Board.count_markpat = count_markpats
なお、judge メソッドから呼び出される is_winner メソッドや、count_markpats から呼び出される count_marks メソッドなどは、ゲーム盤のデータ構造が変化すると 必要がなくなる可能性があるメソッド です。また、それらのメソッドは Marubatsu クラスなどの ListBoard クラスのメソッド以外 から呼び出して 利用することはない ので 抽象メソッドとして定義する必要はありません。
抽象メソッドの一覧
下記は現時点での Board クラスに定義する 抽象メソッドの一覧 です。
| 抽象メソッド | 処理 |
|---|---|
getmark(x, y) |
(x, y) のマスのマークを返す |
setmark(x, y, mark) |
(x, y) のマスに mark を代入する |
board_to_str() |
ゲーム盤を表す文字列を返す |
judge(last_turn, last_move, move_count) |
勝敗判定を計算して返す |
count_markpats(turn, last_turn) |
局面のマークのパターンを返す |
List1dBoard の修正
List1dBoard に対しても上記の 抽象メソッドを定義するように修正 を行う必要がありますが、List1dBoard が行う処理は __init__、getmark、setmark、board_to_str 以外 のメソッドは ListBoard のメソッドと全く同じ です。
以前の記事で説明したように、別のクラスを継承 した 派生クラス は、基底クラスのメソッドをそのまま利用 することができます。また、以前の記事で説明したように 基底クラス のメソッドと 同じ名前のメソッド を 派生クラスに定義 して オーバーライド することで、そのメソッドを呼び出した際に 派生クラスで定義したメソッドが呼び出される ようになります。
従って List1dBoard の定義の修正は ListBoard クラスを継承 し、処理が異なるメソッドだけを定義してオーバーライド することで簡単に修正することができます。
下記のプログラムはそのように List1dBoard クラスを定義するプログラムです。説明と修正箇所は ListBoard クラスのメソッドとの違いです。
- 1 行目:ListBoard クラスを継承するように修正する
-
5 行目:
board属性に 1 次元の list でゲーム盤のデータを初期する - 14、15、18、21 行目:1 次元の list に対する処理を行うように修正する
1 class List1dBoard(ListBoard):
2 def __init__(self, board_size=3, count_linemark=False):
3 self.BOARD_SIZE = board_size
4 self.count_linemark = count_linemark
5 self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
元と同じなので省略
6
7 def setmark(self, x, y, mark):
8 if self.count_linemark:
9 if mark != Marubatsu.EMPTY:
10 diff = 1
11 changedmark = mark
12 else:
13 diff = -1
14 changedmark = self.board[y + x * self.BOARD_SIZE]
元と同じなので省略
15 self.board[y + x * self.BOARD_SIZE] = mark
16
17 def getmark(self, x, y):
18 return self.board[y + x * self.BOARD_SIZE]
19
20 def board_to_str(self):
21 return "".join(self.board)
行番号のないプログラム
-class List1dBoard(Board):
+class List1dBoard(ListBoard):
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
if self.count_linemark:
self.rowcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.colcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.diacount = {
Marubatsu.CIRCLE: [0] * 2,
Marubatsu.CROSS: [0] * 2,
}
def setmark(self, x, y, mark):
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[y + x * self.BOARD_SIZE] = mark
def getmark(self, x, y):
return self.board[y + x * self.BOARD_SIZE]
def board_to_str(self):
return "".join(self.board)
修正箇所
class List1dBoard(ListBoard):
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
+ self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
- self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
元と同じなので省略
def setmark(self, x, y, mark):
if self.count_linemark:
if mark != Marubatsu.EMPTY:
diff = 1
changedmark = mark
else:
diff = -1
- changedmark = self.board[x][y]
+ changedmark = self.board[y + x * self.BOARD_SIZE]
元と同じなので省略
- self.board[x][y] = mark
+ self.board[y + x * self.BOARD_SIZE] = mark
def getmark(self, x, y):
- return self.board[x][y]
+ return self.board[y + x * self.BOARD_SIZE]
def board_to_str(self):
- txt = ""
- for col in self.board:
- txt += "".join(col)
- return txt
+ return "".join(self.board)
動作の確認
上記の修正後に下記のプログラムで 先程と同様の対戦 を List1dBoard を利用した Marubatsu クラス で行うと、実行結果のように 先程とほぼ同じ結果が表示 されることから、List1dBoard の定義が正しく行われたことが確認 できます。
ai_match(ai=[ai8s, ai2s],
mbparams={"boardclass":List1dBoard, "count_linemark": False})
ai_match(ai=[ai8s, ai2s],
mbparams={"boardclass":List1dBoard, "count_linemark": True})
ai_match(ai=[ai14s, ai2s],
mbparams={"boardclass":List1dBoard, "count_linemark": False})
ai_match(ai=[ai14s, ai2s],
mbparams={"boardclass":List1dBoard, "count_linemark": True})
実行結果
ai8s VS ai2s
100%|██████████| 10000/10000 [00:10<00:00, 972.90it/s]
count win lose draw
o 9851 8 141
x 8930 249 821
total 18781 257 962
ratio win lose draw
o 98.5% 0.1% 1.4%
x 89.3% 2.5% 8.2%
total 93.9% 1.3% 4.8%
ai8s VS ai2s
100%|██████████| 10000/10000 [00:05<00:00, 1711.13it/s]
count win lose draw
o 9849 10 141
x 8929 249 822
total 18778 259 963
ratio win lose draw
o 98.5% 0.1% 1.4%
x 89.3% 2.5% 8.2%
total 93.9% 1.3% 4.8%
ai14s VS ai2s
100%|██████████| 10000/10000 [00:12<00:00, 810.29it/s]
count win lose draw
o 9910 0 90
x 8808 0 1192
total 18718 0 1282
ratio win lose draw
o 99.1% 0.0% 0.9%
x 88.1% 0.0% 11.9%
total 93.6% 0.0% 6.4%
ai14s VS ai2s
100%|██████████| 10000/10000 [00:07<00:00, 1380.18it/s]
count win lose draw
o 9902 0 98
x 8831 0 1169
total 18733 0 1267
ratio win lose draw
o 99.0% 0.0% 1.0%
x 88.3% 0.0% 11.7%
total 93.7% 0.0% 6.3%
今回の記事のまとめ
今回の記事では Board クラスの抽象メソッドの追加 を行い、それに従って ListBoard、List1dBoard、Marubatsu クラスを修正 しました。これで準備が整いましたので、次回の記事では今までとは異なるデータ構造でゲーム盤を表現するクラスを定義することにします。
本記事で入力したプログラム
| リンク | 説明 |
|---|---|
| marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
| marubatsu.py | 本記事で更新した marubatsu_new.py |
| ai.py | 本記事で更新した ai_new.py |
| mbtest.py | 本記事で更新した mbtest_new.py |
次回の記事
-
マークの削除は直前の手番を取り消す
unmoveメソッドで行われますが、その場合はゲームの状態が必ずMarubatsu.PLAYINGになるのでjudgeメソッドを呼び出す必要はありません ↩ -
*argsと**kwargsの*と**は、この仮引数が可変長引数であることを表す記号なので、この仮引数の名前はargsとkwargsです。従って、これらの可変長引数を同名の属性に代入する場合は*や**を記述しないargsとkwargsと記述する必要がある点に注意が必要です ↩ -
Marubatsu クラスの
__init__メソッドのargsとkwargsに代入された実引数です ↩