目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
デコレーター式を利用したすべての AI の関数の再定義
前回までの記事で、デコレーター式を利用した ai2s
と ai11s
の定義を行いました。今回の記事では すべての AI の関数 を デコレーター式を利用して定義し直す ことにします。
ai2s
の修正
前回の記事で、ラッパー関数の中で、ラップする関数を呼び出す処理 を、下記のプログラムの 4 行目のように、実引数に mb
と debug
を記述して呼び出す ように修正しました。
1 def ai_by_score(eval_func):
2 @wraps(eval_func)
3 def wrapper(mb_orig, debug=False, *args, rand=True, analyze=False, **kwargs):
省略
4 score = eval_func(mb, debug, *args, **kwargs)
省略
5 return wrapper
そのため、@ai_by_score
というデコレーター式の 下に記述する関数の定義 では、必ず仮引数として mb
と debug
を持つ必要 があります。
しかし、以前の記事で ai2s
をデコレーター式で定義した際は、ai_by_score
を上記のように 修正する前 であったため、下記のプログラムの 2 行目のように、仮引数には mb
のみが記述 されています。
@ai_by_score
def ai2s(mb):
return 0
そのため、下記のプログラムで ai2s
を呼び出すと実行結果のように、エラーが発生 します。エラーメッセージは、ai2s
に対して 1 つの位置引数を記述する必要があるのに対して、2 つの位置引数が記述されているという意味の表示が行われています。
from ai import ai2s
from marubatsu import Marubatsu
mb = Marubatsu()
print(ai2s(mb))
実行結果
略
File c:\Users\ys\ai\marubatsu\132\ai.py:169, in ai_by_score.<locals>.wrapper(mb_orig, debug, rand, analyze, *args, **kwargs)
166 mb.move(x, y)
167 dprint(debug, mb)
--> 169 score = eval_func(mb, debug, *args, **kwargs)
170 dprint(debug, "score", score, "best score", best_score)
171 if analyze:
TypeError: ai2s() takes 1 positional argument but 2 were given
ai2s
を下記のプログラムのように、mb
と debug
の 2 つの仮引数を持つ ように定義する事で、この問題を解決することができます。下記のプログラムの 4、5 行目のラップする関数のように、debug
の値を利用しない 場合でも 仮引数 debug
を必ず記述 する必要があります。
-
2 行目:仮引数
debug=False
を追加する
1 from ai import ai_by_score
2
3 @ai_by_score
4 def ai2s(mb, debug=False):
5 return 0
行番号のないプログラム
from ai import ai_by_score
@ai_by_score
def ai2s(mb, debug=False):
return 0
修正箇所
from ai import ai_by_score
@ai_by_score
-def ai2s(mb):
+def ai2s(mb, debug=False):
return 0
上記の修正後に下記のプログラムを実行すると、エラーが発生しなくなったことが確認できます。
print(ai2s(mb))
実行結果(実行結果はランダムなので下記と異なる場合があります)
(2, 1)
ai2s
の仮引数に関する補足
下記のプログラムのように、ラップする関数の仮引数 に debug
の代わりに *args
と **kwargs
を記述 するという方法もありますが、この方法はお勧めしません。なお、ai2s
と区別するため、下記の関数の名前を ai2s_2
としました。
1 @ai_by_score
2 def ai2s_2(mb, *args, **kwargs):
3 return 0
行番号のないプログラム
@ai_by_score
def ai2s_2(mb, *args, **kwargs):
return 0
修正箇所
@ai_by_score
-def ai2s(mb, debug=False):
+def ai2s_2(mb, *args, **kwargs):
return 0
この方法で ai2s_2
を定義する事で、下記のプログラムのように ai2s_2
を呼び出す際に、2 つ目以降の実引数 に どのような値を記述 して呼び出しても エラーが発生しなくなります。なお、2 つ目の実引数に False
を記述しているのは、ラッパー関数の仮引数 debug
に False
を代入するためです。
ai2s_2(mb, False, 1, 2, a=3, b=4, c=5)
実行結果(実行結果はランダムなので下記と異なる場合があります)
(1, 1)
どのような実引数を記述してもエラーにならないこと は、メリットだと思う人がいるかもしれませんが、これは 大きなデメリット になります。
例えば、ai2s
では、下記のプログラムのように、キーワード引数 debug
の綴り を debig
のように間違って記述 して呼び出すと、実行結果のように予期しないキーワード引数が記述されたことが原因の エラーが発生 します。エラーが発生することで、プログラムが停止する ため、何かがおかしいことに必ず気づくことができます。
print(ai2s(mb, debig=True))
実行結果
略
File c:\Users\ys\ai\marubatsu\132\ai.py:169, in ai_by_score.<locals>.wrapper(mb_orig, debug, rand, analyze, *args, **kwargs)
166 mb.move(x, y)
167 dprint(debug, mb)
--> 169 score = eval_func(mb, debug, *args, **kwargs)
170 dprint(debug, "score", score, "best score", best_score)
171 if analyze:
TypeError: ai2s() got an unexpected keyword argument 'debig'
一方で、下記のプログラムのように、同じ 間違ったキーワード引数を記述 して ai2s_s
を呼び出しても 実行結果のように エラーは発生しません が、本来行いたかった debug=True
を記述した際の デバッグ表示も行われません。エラーが発生しないため、プログラムの処理が続行されるため、間違った処理 が行われていることに 気が付かない可能性が高くなります。
print(ai2s_2(mb, debig=True))
実行結果(実行結果はランダムなので下記と異なる場合があります)
(1, 2)
このように、必要のない実引数を記述してもエラーにならないこと は、メリットよりも デメリットの方が多い ため、必要がない場合 に仮引数に *args
と **kwargs
を 記述しないほうが良い でしょう。
*args
や **kwargs
が必要となる具体例 としては以下のような例が挙げられます。
- 任意の仮引数を持つ関数 に対する ラッパー関数
-
print
のように、任意の数の実引数 に対して処理を行う関数
ai1s
の修正
ai1s
は ルール 1の、「左上から順に空いているマスを探し、最初に見つかったマスに着手 する」という方法で着手を選択しますが、下記のプログラムのように評価値を計算する eval_func
は常に 0
を返し、ai_by_score
の実引数に rand=False
を記述 することで、乱数を使わずに 最初の合法手を選択する という、特殊な処理を行っています。
def ai1s(mb, debug=False):
def eval_func(mb):
return 0
return ai_by_score(mb, eval_func, debug=debug, rand=False)
そのため、上記の eval_func
に対して @ai_by_score
のデコレーター式を記述して ai1s
を定義 すると、ランダムな着手を行う ai2s
と同じ処理を行う関数が定義 されてしまします。
@ai_by_score
のデコレーター式を利用して ルール 1 の ai1s
を定義 するためには、直前の着手 に対して、左上のマスから 右方向に 順番に小さくなっていく ような評価値を計算する関数を定義する必要があります。具体的な評価値として、本記事では下記のプログラムのように、直前の着手 に対して 左上のマスから順に 8
、7
、・・・、0
という評価値を計算する関数を定義することにします。
-
3、4 行目:ゲーム開始時の局面は、直前の着手が存在しないので、その場合は評価値として
0
を返すようにする -
5 ~ 7 行目:直前の着手を (x, y) とすると
x + y * 3
という式で、以前の記事で説明した、下図のように左上のマスから順に0
~8
が割り当てられた 数値座標 を計算できる。8
から数値座標を引き算する ことで、求める評価値を計算することができる
1 @ai_by_score
2 def ai1s(mb, debug=False):
3 if mb.last_move is None:
4 return 0
5 else:
6 x, y = mb.last_move
7 return 8 - (x + y * 3)
行番号のないプログラム
@ai_by_score
def ai1s(mb, debug=False):
if mb.last_move is None:
return 0
else:
x, y = mb.last_move
return 8 - (x + y * 3)
上記の定義後に下記のプログラムを実行すると、ゲーム開始時の局面に対して、ai1s
が左上の (0, 0) を選択することが確認できます。
print(ai1s(mb))
実行結果
(0, 0)
また、下記のプログラムで実引数に analyze=True
を記述して ai1s
を呼び出すと、実行結果のように 左上のマスから順に 8
、7
、・・・、0
という評価値が計算されている ことが確認できます。なお、pprint
は dict の要素 を登録した順ではなく、キーの値によって並べ替えて表示する ので、下記のようにキーの値が 左上のマスから縦方向の順番 で表示され、実行結果では評価値が 8、5、2・・・の順で表示されます。
from pprint import pprint
pprint(ai1s(mb, analyze=True))
実行結果
{'candidate': [(0, 0)],
'score_by_move': {(0, 0): 8,
(0, 1): 5,
(0, 2): 2,
(1, 0): 7,
(1, 1): 4,
(1, 2): 1,
(2, 0): 6,
(2, 1): 3,
(2, 2): 0}}
下記のプログラムのように print
で表示 すれば 登録した順 で dict の要素が表示されます。
print(ai1s(mb, analyze=True))
実行結果
{'candidate': [(0, 0)], 'score_by_move': {(0, 0): 8, (1, 0): 7, (2, 0): 6, (0, 1): 5, (1, 1): 4, (2, 1): 3, (0, 2): 2, (1, 2): 1, (2, 2): 0}}
残りの評価値を利用して着手を選択する AI の再定義
ここまでで、評価値を利用して着手を選択する AI のうち、ai1s
、ai2s
、ai11s
をデコレーター式を利用して定義しなおしました。残り の、ai3s
~ ai10s
、ai10s
~ ai14s
に対しても 同様の方法 で @ai_by_score
のデコレーター式を 利用して定義し直す ことができます。
長くなるので折りたたみますが、下記のプログラムが ai1s
~ ai14s
を @ai_by_score
のデコレーター式を利用して定義しなおしたものです。
ai1s
~ ai14s
の再定義
from marubatsu import Markpat
@ai_by_score
def ai1s(mb, debug=False):
if mb.last_move is None:
return 0
else:
x, y = mb.last_move
return 8 - (x + y * 3)
@ai_by_score
def ai2s(mb, debug=False):
return 0
@ai_by_score
def ai3s(mb, debug=False):
if mb.last_move == (1, 1):
return 1
else:
return 0
@ai_by_score
def ai4s(mb, debug=False):
x, y = mb.last_move
if mb.last_move == (1, 1):
return 10
elif x % 2 == 0 and y % 2 == 0:
return 9 - (x + y * 3)
else:
return 0
@ai_by_score
def ai5s(mb, debug=False):
if mb.status == mb.last_turn:
return 1
else:
return 0
@ai_by_score
def ai6s(mb, debug=False):
# 自分が勝利している場合は、評価値として 1 を返す
if mb.status == mb.last_turn:
return 1
# 相手の手番で相手が勝利できる場合は評価値として -1 を返す
# 横方向と縦方向の判定
for i in range(mb.BOARD_SIZE):
count = mb.count_marks(coord=[0, i], dx=1, dy=0)
if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
return -1
count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
return -1
# 左上から右下方向の判定
count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
return -1
# 右上から左下方向の判定
count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
return -1
# それ以外の場合は評価値として 0 を返す
return 0
@ai_by_score
def ai7s(mb, debug=False):
# 真ん中のマスに着手している場合は、評価値として 2 を返す
if mb.last_move == (1, 1):
return 2
# 自分が勝利している場合は、評価値として 1 を返す
if mb.status == mb.last_turn:
return 1
# 相手の手番で相手が勝利できる場合は評価値として -1 を返す
# 横方向と縦方向の判定
for i in range(mb.BOARD_SIZE):
count = mb.count_marks(coord=[0, i], dx=1, dy=0)
if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
return -1
count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
return -1
# 左上から右下方向の判定
count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
return -1
# 右上から左下方向の判定
count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
return -1
# それ以外の場合は評価値として 0 を返す
return 0
@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.enum_markpats()
# 相手が勝利できる場合は評価値として -1 を返す
if Markpat(last_turn=0, turn=2, empty=1) in markpats:
return -1
# 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
elif Markpat(last_turn=2, turn=0, empty=1) in markpats:
return 1
# それ以外の場合は評価値として 0 を返す
else:
return 0
@ai_by_score
def ai9s(mb, debug=False):
# 真ん中のマスに着手している場合は、評価値として 4 を返す
if mb.last_move == (1, 1):
return 4
# 自分が勝利している場合は、評価値として 3 を返す
if mb.status == mb.last_turn:
return 3
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合は評価値として -1 を返す
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -1
# 次の自分の手番で自分が必ず勝利できる場合は評価値として 2 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return 2
# 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
return 1
# それ以外の場合は評価値として 0 を返す
else:
return 0
@ai_by_score
def ai10s(mb, debug=False):
# 真ん中のマスに着手している場合は、評価値として 300 を返す
if mb.last_move == (1, 1):
return 300
# 自分が勝利している場合は、評価値として 200 を返す
if mb.status == mb.last_turn:
return 200
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合は評価値として -100 を返す
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -100
# 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return 100
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 次の自分の手番で自分が勝利できる場合は評価値に 1 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += 1
# 「自 1 敵 0 空 2」の数だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)]
# 計算した評価値を返す
return score
@ai_by_score
def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):
# 真ん中のマスに着手している場合は、評価値として 300 を返す
if mb.last_move == (1, 1):
return 300
# 自分が勝利している場合は、評価値として 200 を返す
if mb.status == mb.last_turn:
return 200
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合は評価値として -100 を返す
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -100
# 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return 100
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += score_201
# 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
# 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
# 計算した評価値を返す
return score
@ai_by_score
def ai12s(mb, debug=False, score_victory=300, score_sure_victory=200, \
score_defeat=-100, score_special=100, score_201=2, \
score_102=0.5, score_012=-1):
# 自分が勝利している場合
if mb.status == mb.last_turn:
return score_victory
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return score_defeat
# 次の自分の手番で自分が必ず勝利できる場合
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return score_sure_victory
# 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
if mb.board[1][1] == Marubatsu.CROSS and \
(mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
(mb.board[1][0] == Marubatsu.CROSS or \
mb.board[0][1] == Marubatsu.CROSS or \
mb.board[2][1] == Marubatsu.CROSS or \
mb.board[1][2] == Marubatsu.CROSS) and \
mb.move_count == 4:
return score_special
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += score_201
# 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
# 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
# 計算した評価値を返す
return score
@ai_by_score
def ai13s(mb, debug=False, score_victory=300, score_sure_victory=200, \
score_defeat=-100, score_special=100, score_201=2, \
score_102=0.5, score_012=-1):
# 自分が勝利している場合
if mb.status == mb.last_turn:
return score_victory
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return score_defeat * markpats[Markpat(last_turn=0, turn=2, empty=1)]
# 次の自分の手番で自分が必ず勝利できる場合
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return score_sure_victory
# 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
if mb.board[1][1] == Marubatsu.CROSS and \
(mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
(mb.board[1][0] == Marubatsu.CROSS or \
mb.board[0][1] == Marubatsu.CROSS or \
mb.board[2][1] == Marubatsu.CROSS or \
mb.board[1][2] == Marubatsu.CROSS) and \
mb.move_count == 4:
return score_special
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += score_201
# 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
# 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
# 計算した評価値を返す
return score
@ai_by_score
def ai14s(mb, debug=False, score_victory=300, score_sure_victory=200, \
score_defeat=-100, score_special=100, score_201=2, \
score_102=0.5, score_012=-1):
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 自分が勝利している場合
if mb.status == mb.last_turn:
return score_victory
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合は評価値を加算する
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
score = score_defeat * markpats[Markpat(last_turn=0, turn=2, empty=1)]
# 次の自分の手番で自分が必ず勝利できる場合
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return score_sure_victory
# 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
if mb.board[1][1] == Marubatsu.CROSS and \
(mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
(mb.board[1][0] == Marubatsu.CROSS or \
mb.board[0][1] == Marubatsu.CROSS or \
mb.board[2][1] == Marubatsu.CROSS or \
mb.board[1][2] == Marubatsu.CROSS) and \
mb.move_count == 4:
return score_special
# 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += score_201
# 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
# 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
# 計算した評価値を返す
return score
なお、再定義した AI の関数が 正しく動作するか どうかの 検証は次回の記事 で行います。
評価値を計算しない AI の関数に対するデコレーター式による再定義
下記のプログラムの ai2
などの、評価値を計算せずに着手の選択を行う AI の関数 に対しては、ai_by_score
を利用 してラッパー関数を 定義する事はできません。
def ai2(mb):
legal_moves = mb.calc_legal_moves()
return choice(legal_moves)
評価値を計算しない AI に対するラッパー関数 は、ai_by_score
と同様に、ラップする AI の関数に対して、下記の機能を追加する必要 があります。
- デバッグ表示を行うことを表す仮引数
debug
を追加する - ランダムな着手を行うかどうかを表す仮引数
rand
を追加する - 候補手の一覧1を返り値として返すことを表す仮引数
analyze
を追加する
この中で、仮引数 analyze
を追加 するという 機能の拡張 は、以前の記事で下記の ai3
に対して既に行っています。
def ai3(mb):
if mb.board[1][1] == Marubatsu.EMPTY:
return 1, 1
legal_moves = mb.calc_legal_moves()
return choice(legal_moves)
上記のプログラムに対して、下記のように修正 することで、仮引数 analyze
に代入された値によって候補手の一覧を返り値として返すように ai3
の機能を拡張 しています。
1 def ai3(mb, analyze=False):
2 if mb.board[1][1] == Marubatsu.EMPTY:
3 candidate = [(1, 1)]
4 else:
5 candidate = mb.calc_legal_moves()
6 if analyze:
7 return {
8 "candidate": candidate,
9 "score_by_move": None
10 }
11 else:
12 return choice(candidate)
そこで、上記の修正を参考 に、最初に 仮引数 analyze
に関する機能を追加 したラッパー関数を作成する デコレーターの関数を定義 する事にし、その後で仮引数 debug
と rand
の機能を追加したデコレーターの関数を定義する事にします。
仮引数 analyze
の機能の追加
上記のプログラムの 6 ~ 12 行目 が ai3
に対して、仮引数 analyze
の処理を追加した部分 です。従って、ラッパー関数を定義して ai3
の機能の拡張を行う場合は、この部分の処理をラッパー関数の中に記述 します。
具体的には、デコレータの関数を以下のように定義します。なお、デコレーターの関数の名前を ai_by_candidate
とした理由はこの後で説明します。
-
6 行目:仮引数
mb
、*args
、analyze
、**args
を持つラッパー関数を定義する。ラップする関数が任意の仮引数を持つことができるように 仮引数*args
と**kwargs
を記述 する。また、仮引数analyze
は ラッパー関数のみで使われる仮引数 ので、*args
より後で記述する -
7 行目:ラップする関数を呼び出し、返り値を
candidate
に代入する -
8 ~ 14 行目:
ai3
の機能を拡張する、上記の 6 ~ 12 行目と同じ処理を行う
1 from functools import wraps
2 from random import choice
3
4 def ai_by_candidate(func):
5 @wraps(eval_func)
6 def wrapper(mb, *args, analyze=False, **kwargs):
7 candidate = func(mb, *args, **kwargs)
8 if analyze:
9 return {
10 "candidate": candidate,
11 "score_by_move": None
12 }
13 else:
14 return choice(candidate)
15
16 return wrapper
行番号のないプログラム
from functools import wraps
from random import choice
def ai_by_candidate(func):
@wraps(eval_func)
def wrapper(mb, *args, analyze=False, **kwargs):
candidate = func(mb, *args, **kwargs)
if analyze:
return {
"candidate": candidate,
"score_by_move": None
}
else:
return choice(candidate)
return wrapper
@ai_by_candidate
のデコレーター式を利用して、ai3
を下記のプログラムのように定義し直すことができます。上記のプログラムの ラッパー関数の 6 行目 で ラップする関数を呼び出し、返り値として候補手の一覧を受け取っている ので、下記のプログラムのように 7 行目で 候補手の一覧を表す candidate
を返す処理を記述 する必要があります。
1 @ai_by_candidate
2 def ai3(mb):
3 if mb.board[1][1] == Marubatsu.EMPTY:
4 candidate = [(1, 1)]
5 else:
6 candidate = mb.calc_legal_moves()
7 return candidate
行番号のないプログラム
@ai_by_candidate
def ai3(mb):
if mb.board[1][1] == Marubatsu.EMPTY:
candidate = [(1, 1)]
else:
candidate = mb.calc_legal_moves()
return candidate
上記の実行後に下記のプログラムを実行することで、実行結果から ai3
が正しく動作することが確認できます。なお、ai3
は 真ん中のマスに優先的に着手を行う ルール 3 を実装した AI なので、ゲーム開始時の局面では必ず (1, 1) に着手 を行います。
print(ai3(mb))
実行結果
(1, 1)
また、下記のプログラムで実引数に analyze=True
を記述して ai3
を呼び出した場合も、実行結果から正しく動作することが確認できます。
print(ai3(mb, analyze=True))
実行結果
{'candidate': [(1, 1)], 'score_by_move': None}
@ai_by_candidate
を利用した ai2
の定義
@ai_by_candidate
を利用した ai2
を定義する際に、下記のプログラムのように 元の ai2
の定義をそのまま記述 することは できません。
@ai_by_candidate
def ai2(mb):
legal_moves = mb.calc_legal_moves()
return choice(legal_moves)
上記の実行後に下記のプログラムを実行すると、実行結果のように座標ではなく、0
などの数値が表示 されます。なお、表示される数値は 0、1、2 の中からランダムに選ばれます。
print(ai2(mb))
実行結果(実行結果はランダムなので下記と異なる場合があります)
0
このようなことが起きるのは、ai_by_candidate
が定義する ラッパー関数 が、ラップする関数の返り値 として 候補手の一覧を必要とする からです。上記の ai2
の ラップする関数 は、候補手の一覧ではなく、候補手の中からランダムに選択した 1 つの合法手を返します。例えば (0, 1)
という合法手が 返り値として得られた場合 は、ai_by_candidate
の wrapper
の中で choice((0, 1))
が返り値として返されるので、0
または 1
の いずれかの数値が返ります。このように、上記の ai2
は、合法手 の x 座標または y 座標のどちらかを返り値として返す という間違った処理を行います。
従って、ai_by_candidate
をデコレーターとして利用する場合は、ラップする関数 が返り値として 候補手の一覧のデータを返す 必要があります。下記は、そのように ai2
を定義したプログラムです。なお、デコレーターの関数の名前 を ai_by_candidate
としたのは、候補手(candidate)を返す関数に対する AI のラッパー関数を作成 する処理を行うからです。
@ai_by_candidate
def ai2(mb):
return mb.calc_legal_moves()
上記のように ai2
を修正することで、下記のプログラムのように ai2
が正しい処理を行うようになったことが確認できます。
print(ai2(mb))
実行結果(実行結果はランダムなので下記と異なる場合があります)
(1, 1)
仮引数 debug
と rand
の機能の追加
次に、ai_by_candidate
対して 仮引数 debug
と rand
の機能を追加 することにします。debug
に True
が代入 されていた場合は、候補手の一覧を表示 することにします。
下記は、そのように ai_by_candidate
を修正したプログラムです。
-
5 行目:ラッパー関数の仮引数
*args
の前に仮引数debug
を、後にrand
を追加する -
6 行目:
ay_by_score
の場合と同様に、ラップする関数を呼び出す際に、2 つ目の実引数にdebug
を記述するように修正する -
7 行目:
dprint
を使って、debug
がTrue
の場合に候補手の一覧を表示する -
14 ~ 17 行目:
rand
がTrue
の場合は候補手の中からランダムに選択した合法手を返し、False
の場合は先頭の候補手を返すように修正する
1 from ai import dprint
2
3 def ai_by_candidate(func):
4 @wraps(func)
5 def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
6 candidate = func(mb, debug, *args, **kwargs)
7 dprint(debug, "candidate", candidate)
8 if analyze:
9 return {
10 "candidate": candidate,
11 "score_by_move": None
12 }
13 else:
14 if rand:
15 return choice(candidate)
16 else:
17 return candidate[0]
18
19 return wrapper
行番号のないプログラム
from functools import wraps
from ai import dprint
def ai_by_candidate(func):
@wraps(func)
def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
candidate = func(mb, debug, *args, **kwargs)
dprint(debug, "candidate", candidate)
if analyze:
return {
"candidate": candidate,
"score_by_move": None
}
else:
if rand:
return choice(candidate)
else:
return candidate[0]
return wrapper
修正箇所
from ai import dprint
def ai_by_candidate(func):
@wraps(func)
- def wrapper(mb, *args, analyze=False, **kwargs):
+ def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
- candidate = func(mb, *args, **kwargs)
+ candidate = func(mb, debug, *args, **kwargs)
+ dprint(debug, "candidate", candidate)
if analyze:
return {
"candidate": candidate,
"score_by_move": None
}
else:
- return choice(candidate)
+ if rand:
+ return choice(candidate)
+ else:
+ return candidate[0]
return wrapper
上記の修正後に、下記のプログラムで ai2
を定義し直します。ラップする関数を呼び出す際に 2 つ目の実引数に debug
を記述する ように修正したので、2 行目では仮引数 debug
を追加する 必要があります。
1 @ai_by_candidate
2 def ai2(mb, debug=False):
3 return mb.calc_legal_moves()
行番号のないプログラム
@ai_by_candidate
def ai2(mb, debug=False):
return mb.calc_legal_moves()
修正箇所
@ai_by_candidate
-def ai2(mb):
+def ai2(mb, debug=False):
return mb.calc_legal_moves()
上記の修正後に下記のプログラムで debug=True
と rand=False
を記述して ai2
を呼び出すと、実行結果のように 候補手の一覧が表示 され、必ず最初の候補手である (0, 0) が返り値として返るようになります。何度か下記のプログラムを実行して常に (0, 0) が返るようになったことを確認して下さい。
print(ai2(mb, rand=False, debug=True))
実行結果
candidate [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
(0, 0)
残りの評価値を利用しない AI の再定義
長くなるので折りたたみますが、下記のプログラムが ai1
~ ai7
、ai_gt1
~ ai_gt6
を @ai_by_candidate
のデコレーター式を利用して定義しなおしたものです。
その際に、先ほど説明したように、修正前の関数に対して、返り値として候補手の一覧を返す ように修正する必要があります。また、元のプログラムが return 1, 1
のように、一つの合法手を返す場合 は、return [(1, 1)]
のように、合法手を 1 つだけ持つ list の形式で返す ように修正する必要がある点に注意して下さい。
ai1
~ ai7
、ai_gt1
~ ai_gt6
の再定義
from copy import deepcopy
@ai_by_candidate
def ai1(mb, debug=False):
for y in range(mb.BOARD_SIZE):
for x in range(mb.BOARD_SIZE):
if mb.board[x][y] == Marubatsu.EMPTY:
return [(x, y)]
@ai_by_candidate
def ai2(mb, debug=False):
legal_moves = mb.calc_legal_moves()
return legal_moves
@ai_by_candidate
def ai3(mb, debug=False):
if mb.board[1][1] == Marubatsu.EMPTY:
candidate = [(1, 1)]
else:
candidate = mb.calc_legal_moves()
return candidate
@ai_by_candidate
def ai4(mb, debug=False):
if mb.board[1][1] == Marubatsu.EMPTY:
return [(1, 1)]
for y in range(0, 3, 2):
for x in range(0, 3, 2):
if mb.board[x][y] == Marubatsu.EMPTY:
return [(x, y)]
return mb.calc_legal_moves()
@ai_by_candidate
def ai5(mb_orig, debug=False):
legal_moves = mb_orig.calc_legal_moves()
# すべての合法手について繰り返し処理を行う
for move in legal_moves:
# mb_orig をコピーし、コピーしたもの対して着手を行う
mb = deepcopy(mb_orig)
x, y = move
mb.move(x, y)
# 勝利していれば、その合法手を返り値として返す
if mb.status == mb_orig.turn:
return [move]
return legal_moves
@ai_by_candidate
def ai6(mb_orig, debug=False):
# mb_orig の合法手の中で、自分が勝利できる合法手があればそこに着手する
legal_moves = mb_orig.calc_legal_moves()
# 合法手が 1 つしかない場合は、その合法手を返り値として返す
if len(legal_moves) == 1:
return legal_moves
# 合法手の中で、勝てるマスがあれば、その合法手を返り値として返す
for move in legal_moves:
mb = deepcopy(mb_orig)
x, y = move
mb.move(x, y)
if mb.status == mb_orig.turn:
return [move]
# 〇 が勝利する合法手が存在しないことが確定した場合は、
# 現在の局面を相手の手番とみなし、合法手の中で、相手が着手して
# 勝利するマスがあれば、その合法手を返り値として返す
for move in legal_moves:
mb = deepcopy(mb_orig)
# 現在の局面の手番を入れ替える
mb.turn = Marubatsu.CROSS if mb.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
enemy_turn = mb.turn
x, y = move
mb.move(x, y)
if mb.status == enemy_turn:
return [move]
return legal_moves
@ai_by_candidate
def ai7(mb, debug=False):
if mb.board[1][1] == Marubatsu.EMPTY:
return [(1, 1)]
return ai6(mb, debug=debug, analyze=True)["candidate"]
@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree=None):
node = mbtree.root
for move in mb.records[1:]:
node = node.children_by_move[move]
bestmoves = []
for move, childnode in node.children_by_move.items():
if node.score == childnode.score:
bestmoves.append(move)
return bestmoves
@ai_by_candidate
def ai_gt2(mb, debug=False, mbtree=None):
node = mbtree.root
for move in mb.records[1:]:
node = node.children_by_move[move]
return node.bestmoves
@ai_by_candidate
def ai_gt3(mb, debug=False, mbtree=None):
node = mbtree.nodelist_by_mb[tuple(mb.records)]
return node.bestmoves
@ai_by_candidate
def ai_gt4(mb, debug=False, mbtree=None):
return mbtree.bestmoves_by_mb[tuple(mb.records)]
@ai_by_candidate
def ai_gt5(mb, debug=False, bestmoves=None):
return bestmoves[tuple(mb.records)]
@ai_by_candidate
def ai_gt6(mb, debug=False, bestmoves_by_board=None):
return bestmoves_by_board[mb.board_to_str()]
いくつかの関数の再定義について補足します。
ai7
の再定義
修正前の ai7
は下記のプログラムのように、その中で ai6
を呼び出した返り値を返しています が、ai6
の返り値は候補手の一覧ではない ので、この関数の前に @ai_by_candidate
のデコレーター式を記述 しても うまくいきません。
def ai7(mb):
if mb.board[1][1] == Marubatsu.EMPTY:
return 1, 1
return ai6(mb)
そのため、@ai_by_candidate
のデコレーター式で ai7
を定義し直す 場合は、下記のプログラムの 5 行目のように 候補手の一覧を返すように修正 する必要があります。
-
5 行目:実引数に
analyze=True
を記述してai6
を呼び出すことで、ai6
の返り値の中に候補手の一覧が含まれる ように修正する。また、その 返り値の dict のcandidate
というキーの値に 候補手の一覧が代入 されているので、そのキーの値を返り値として返す ように修正する
1 @ai_by_candidate
2 def ai7(mb, debug=False):
3 if mb.board[1][1] == Marubatsu.EMPTY:
4 return [(1, 1)]
5 return ai6(mb, debug=debug, analyze=True)["candidate"]
ai_gt1
~ ai_gt6
の再定義
修正前の ai_gt1
~ ai_gt6
は、下記のプログラムのように mb
以外の仮引数 を持ちます。
def ai_gt1(mb, mbtree):
略
@ai_by_candidate
のデコレーター式 を利用する場合は、2 つ目の仮引数に debug
を記述 する必要がありますが、下記のプログラムのように 仮引数 debug
をデフォルト引数として定義 すると、デフォルト引数より後 に 通常の仮引数 mbtree
が定義 されているため、エラーが発生 します。
@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree):
略
このような場合は、下記のプログラムの mbtree=None
のように、仮引数 mbtree
に何らかのデフォルト値を設定 する必要があります。ai_gt1
の処理では mbtree
に適切なデータが代入されている必要があるため、mbtree
に対応する実引数を記述せずに ai_gt1
を呼び出すと、ai_gt1
の中の処理でエラーが発生します。
@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree=None):
略
下記のプログラムのように、debug
をデフォルト引数としないようにするという方法もありますが、この場合は ai_gt1
を呼び出す際に、debug
に対する実引数を省略できなくなる点が不便 になります。
@ai_by_candidate
def ai_gt1(mb, debug, mbtree):
略
再定義した AI の関数の検証
再定義した AI の関数 正しく動作するか を、それぞれの AI の関数を呼び出して検証するのは、AI の数が多いため大変です。本記事では他の検証方法として、gui_play
で AI を選択して AI どうしの対戦を行う ことで 検証する ことにします。ただし、gui_play
では、ai.py
の中で定義されている AI の関数 を使って AI の対戦を行っています。現状の ai.py には修正前の AI の関数が定義 されているので、このままでは gui_play
を利用して、再定義した AI の関数の検証を行うことはできません。
今回の記事で再定義した AI の関数 は ai_new.py に記述し、その内容は 次回の記事の ai.py に反映する ので、再定義した AI の 関数の検証は次回の記事で行う ことにします。
今回の記事の内容
今回の記事では、評価値を利用 して着手を選択する AI の関数 をデコレーター式 @ai_by_score
を利用して再定義 しました。
また、評価値を利用せず に着手を選択する AI の関数 に対する デコレータの関数 ay_by_candidate
を定義 し、評価値を利用せずに着手を選択する AI の関数を デコレーター式 @ai_by_candidate
を利用して再定義 しました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
ai.py | 本記事で更新した ai_new.py |
次回の記事