目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
Marubatsu_GUI クラスの改良の続き
以前の記事で、Marubatsu_GUI クラスの改良を開始しました。今回の記事ではその中の最後の、下記の改良の実装を開始します。
- ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できるようにする
AI が計算する評価値の表示のメリット
現状では、下記のプログラムのように、ai14s
などの一部の AI の関数はキーワード引数 debug=True
を記述して AI の関数を呼び出すことで、それぞれの 合法手を着手した場合に計算される評価値を表示する ようになっています。
from marubatsu import Marubatsu
from ai import ai14s
mb = Marubatsu()
ai14s(mb, debug=True)
実行結果
Start ai_by_score
Turn o
...
...
...
legal_moves [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (0, 0)
Turn x
O..
...
...
defaultdict(<class 'int'>,
{Markpat(last_turn=0, turn=0, empty=3): 5,
Markpat(last_turn=1, turn=0, empty=2): 3})
score 1.5 best score -inf
UPDATE
best score 1.5
best moves [(0, 0)]
====================
move (1, 0)
Turn x
.O.
...
...
略
上記のようなデバッグの表示は、評価値を計算する際の 詳細なデータが表示されるという点では便利 ですが、一覧性に欠ける という欠点があります。そこで、ゲーム盤のそれぞれのマスに、そのマスに着手を行った局面に対して AI が計算する評価値を表示するという工夫を行うことにします。
また、ai1
のように、評価値を計算しない AI の場合 はゲーム盤のマスに、その AI が計算した 候補手であることがわかるような、何らかの表示を行う ことにします。
評価値を表示するために必要な処理の実装
現状の AI の関数 は、以下の返り値を返します。
- 通常は、合法手の中から AI が選択した 1 つの着手を返す
- キーワード引数
candidate=True
を記述して呼び出した場合は、その AI が最善手とみなした 候補手の一覧を list で返す。ただし、現状では、一部の AI の関数のみこの機能が実装されている
合法手を着手した局面の評価値を表示するためには、上記以外に、それぞれの 合法手を着手した局面の評価値の情報 を AI の関数が 返り値として返す必要 があります。
そこで、AI の関数を以下のように修正することにします。
- 通常は、合法手の中から AI 選択した 1 つの着手を返す点は変わらない
- デフォルト値を
Falst
とする仮引数analyze
を追加し、True
が代入された場合 は、「その AI が最善手とみなした 候補手の一覧」と「それぞれの 合法手を着手した局面の評価値」を表すデータを下記の dict で返すようにする。このデータは、AI の処理を分析(analyze)するために利用できるので、キーワード引数の名前をanalyze
とした-
candidate
というキーの値に、その AI が最善手とみなした 候補手の一覧を表す list を代入する -
score_by_move
というキーの値に、それぞれの 合法手を着手した局面の評価値を表す dict を代入する。ただし、ai1
のように、評価値を計算しない AI の関数の場合は、None
を代入 する
-
-
仮引数
candidate
の機能は、仮引数analyze
の機能に含まれるので 削除する
ai3s
の修正
まず、評価値を計算する関数から修正 することにします。そのような関数は ai1s
から ai14s
まで 14 種類もありますが、真ん中のマスに優先的に着手を行うという単純な ルール3 を実装した ai3s
の修正を最初に行う ことにします。
具体的には ai3s
は下記のプログラムのような単純な方法で評価値の計算を行います。
- 中央の (1, 1) に着手が行われていた場合は評価値として 1 を計算する
- それ以外のマスに着手を行がわれていた場合は評価値として 0 を計算する
def ai3s(mb, debug=False, candidate=False):
def eval_func(mb):
if mb.last_move == (1, 1):
return 1
else:
return 0
return ai_by_score(mb, eval_func, debug=debug, candidate=candidate)
左上から順に空いているマスを探し、最初に見つかったマスに着手するという、ルール 1 を実装した ai1s
を選択しなかった のは、キーワード引数 rand=False
を記述して ai_by_score
を呼び出すことで先頭の合法手を選択するという、評価値をそのまま利用しない特殊な処理を行っているから です。そのため、ai1s
の修正は、他の ai2s
~ ai14s
とは若干異なる方法で行う必要があり、その修正は次回の記事で行います。
ランダムな着手を行うという、ルール 2 を実装した ai2s
を選択しなかった のは、常に全ての合法手が最善手となり、全ての空いているマスに 0
という評価値が表示されるため、見た目があまり面白くない からです。
ai_gt1
~ ai_gt6
も評価値を元に候補手を計算しますが、その場で評価値を計算していない 点が ai3s
と異なるため、修正方法は ai3s
と大きく異なります。それらの修正は次回の記事で行います。
まず、下記のプログラムのように ai3s
を修正します。
-
1 行目:仮引数
candidate
をanalyze
に修正する -
7 行目:キーワード引数
candidate=candidate
をanalyze=analyze
に修正する
1 def ai3s(mb, debug=False, analyze=False):
2 if mb.last_move == (1, 1):
3 return 1
4 else:
5 return 0
6
7 return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)
行番号のないプログラム
def ai3s(mb, debug=False, analyze=False):
def eval_func(mb):
if mb.last_move == (1, 1):
return 1
else:
return 0
return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)
修正箇所
-def ai3s(mb, debug=False, candidate=False):
+def ai3s(mb, debug=False, analyze=False):
def eval_func(mb):
if mb.last_move == (1, 1):
return 1
else:
return 0
- return ai_by_score(mb, eval_func, debug=debug, candidate=candidate)
+ return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)
ai_by_score
の修正
次に、ai3s
の 7 行目で呼び出す ai_by_score
を以下のプログラムのように修正します。
-
5 行目:仮引数
candidate
をanalyze
に修正する -
7、8 行目:
analyze
がTrue
の場合に、「それぞれの合法手を着手した局面の評価値」を記録するscore_by_move
を空の dict で初期化する -
12、13 行目:
analyze
がTrue
の場合に、move
を着手した局面の評価値をscore_by_move
に記録する -
14 ~ 18 行目:
analyze
がTrue
の場合に先程説明した dict を返り値として返すように修正する
1 from copy import deepcopy
2 from random import choice
3 from ai import dprint
4
5 def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):
元と同じなので省略
6 best_moves = []
7 if analyze:
8 score_by_move = {}
9 for move in legal_moves:
元と同じなので省略
10 score = eval_func(mb)
11 dprint(debug, "score", score, "best score", best_score)
12 if analyze:
13 score_by_move[move] = score
元と同じなので省略
14 if analyze:
15 return {
16 "candidate": best_moves,
17 "score_by_move": score_by_move,
18 }
19 elif rand:
20 return choice(best_moves)
21 else:
22 return best_moves[0]
行番号のないプログラム
from copy import deepcopy
from random import choice
from ai import dprint
def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):
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 = deepcopy(mb_orig)
x, y = move
mb.move(x, y)
dprint(debug, mb)
score = eval_func(mb)
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]
修正箇所
from copy import deepcopy
from random import choice
from ai import dprint
-def ai_by_score(mb_orig, eval_func, debug=False, rand=True, candidate=False):
+def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):
元と同じなので省略
best_moves = []
+ if analyze:
+ score_by_move = {}
for move in legal_moves:
元と同じなので省略
score = eval_func(mb)
dprint(debug, "score", score, "best score", best_score)
+ if analyze:
+ score_by_move[move] = score
元と同じなので省略
- if candidate:
- return 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]
上記の修正後に、下記のプログラムでキーワード引数 analyze=True
を記述してゲーム開始時の局面に対して ai3s
を呼び出すと、実行結果のように下記のような候補手の一覧と、それぞれの合法手を着手した局面の評価値を記録した dict が返されることが確認できます。なお、わかりやすさを重視して、結果を pprint
で表示しました。
- 候補手の一覧は (1, 1) のみを要素とする list
- 局面の評価値は (1, 1) のキーの値が 1、それ以外のキーの値が 0 となる dict
from pprint import pprint
pprint(ai3s(mb, analyze=True))
実行結果
{'candidate': [(1, 1)],
'score_by_move': {(0, 0): 0,
(0, 1): 0,
(0, 2): 0,
(1, 0): 0,
(1, 1): 1,
(1, 2): 0,
(2, 0): 0,
(2, 1): 0,
(2, 2): 0}}
また、下記のプログラムのように (1, 1) に着手を行った後で ai3s
を実行すると、実行結果のように全ての合法手が候補手に、全ての着手に対する局面の評価値が 0 になることが確認できます。
mb.move(1, 1)
pprint(ai3s(mb, analyze=True))
実行結果
{'candidate': [(0, 0), (1, 0), (2, 0), (0, 1), (2, 1), (0, 2), (1, 2), (2, 2)],
'score_by_move': {(0, 0): 0,
(0, 1): 0,
(0, 2): 0,
(1, 0): 0,
(1, 2): 0,
(2, 0): 0,
(2, 1): 0,
(2, 2): 0}}
ai3
の修正
次に、評価値を計算しない AI として、ai3s
と同じルール 3で候補手を計算する下記のプログラムで定義される ai3
を修正することにします。
def ai3(mb):
if mb.board[1][1] == Marubatsu.EMPTY:
return 1, 1
legal_moves = mb.calc_legal_moves()
return choice(legal_moves)
プログラムの改良
評価値を計算しない AI では、何らかの手順で候補手の一覧を計算し、その中からランダムに 1 つを選択するという処理を行う必要があります。
ai3
では、(1, 1) に着手が行われていない場合は、(1, 1) を返し、そうでなければ合法手の一覧からランダムに 1 を選択して返すという処理を行っていますが、この 2 つの処理を下記の方法で 1 つにまとめることができます。
- (1, 1) のマスが開いている場合は、候補手の一覧として
(1, 1)
のみを要素として持つ[(1, 1)]
という list を計算する - (1, 1) のマスが開いていない場合は合法手の一覧を候補手の一覧とする
- 候補手の一覧の中からランダムに 1 つを選択して返す
上記の手順で正しい処理が行われる理由は、[(1, 1)]
のような、要素が 1 つしかない list に対して choice
でランダムに要素を選択した場合は、必ず (1, 1)
が選択されるからです。下記は、そのように ai3
を修正したプログラムです。
-
2、3 行目:(1, 1) のマスが開いている場合に候補手を表す
candidate
に(1, 1)
のみを要素とする list を代入するように修正する -
4、5 行目:(1, 1) のマスが開いていない場合は
candidate
に合法手の一覧を代入するように修正する。 -
6 行目:
choice
を利用して候補手の中からランダムに一つを選択した着手を返す
1 def ai3(mb):
2 if mb.board[1][1] == Marubatsu.EMPTY:
3 candidate = [(1, 1)]
4 else:
5 candidate = mb.calc_legal_moves()
6 return choice(candidate)
修正箇所
def ai3(mb):
if mb.board[1][1] == Marubatsu.EMPTY:
- return (1, 1)]
+ candidate = [(1, 1)]
- legal_moves = mb.calc_legal_moves()
- return choice(legal_moves)
+ else:
+ candidate = mb.calc_legal_moves()
+ return choice(candidate)
3 行目を candidate = (1, 1)
のように記述して ai3
を呼び出してもエラーは発生しませんが、(1, 1) のマスが開いていた場合は 返り値に 1
が返る という バグが発生する 点に注意して下さい。
その理由は、choice
が list や tuple などの 反復可能オブジェクトの中からランダムに 1 つの要素を選択する という処理を行うからです。candidate
に (1, 1)
が代入されていた場合は、choice((1, 1))
が実行され、(1, 1)
という tuple の中からランダムに 1 つの要素が選択されます。(1, 1)
の要素はどちらも 1
なので、必ず 1
が返される ことになります。
仮引数 analyze
の追加
次に、下記のプログラムのように ai3
に仮引数 analyze
を追加し、analyze
が True
の場合の処理を記述します。上記で ai3
を修正した理由 は、下記のプログラムの 6 ~ 12 行目のように、返り値を計算する処理 を (1, 1) のマスが開いているかどうかに関わらず、一つにまとめることができるようにするため です
-
1 行目:仮引数
analyze
を追加する -
6 ~ 10 行目:
analyze
がTrue
の場合に返り値として返す dict を計算する。ai3
は評価値を計算しないので、score_by_move
のキーの値はNone
とする -
11、12 行目:
analyze
がFalse
の場合は元と同じ処理を行う
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)
行番号のないプログラム
def ai3(mb, analyze=False):
if mb.board[1][1] == Marubatsu.EMPTY:
candidate = [(1, 1)]
else:
candidate = mb.calc_legal_moves()
if analyze:
return {
"candidate": candidate,
"score_by_move": None
}
else:
return choice(candidate)
修正箇所
-def ai3(mb):
+def ai3(mb, analyze=False):
if mb.board[1][1] == Marubatsu.EMPTY:
candidate = [(1, 1)]
else:
candidate = mb.calc_legal_moves()
- return choice(candidate)
+ if analyze:
+ return {
+ "candidate": candidate,
+ "score_by_move": None
+ }
+ else:
+ return choice(candidate)
上記の修正後に、下記のプログラムでキーワード引数 analyze=True
を記述して ai3
を実行すると、実行結果のように正しいデータが返されることが確認できます。
mb.restart()
pprint(ai3(mb, analyze=True))
実行結果
{'candidate': [(1, 1)], 'score_by_move': None}
また、下記のプログラムのように (1, 1) に着手を行った後で ai3
を実行すると、実行結果のように全ての合法手が候補手になることが確認できます。
mb.move(1, 1)
pprint(ai3(mb, analyze=True))
実行結果
{'candidate': [(0, 0), (1, 0), (2, 0), (0, 1), (2, 1), (0, 2), (1, 2), (2, 2)],
'score_by_move': None}
残りの修正
この後で行う必要がある修正は以下の通りです。
-
ai_by_score
の仮引数candidate
を廃止したので、キーワード引数candidate=candidate
を記述して AI の関数を呼び出す処理を修正 する - 残りの AI の関数を修正する
キーワード引数 candidate=candidate
を記述して AI の関数を呼び出す処理の修正
1 つ目の、キーワード引数 candidate=candidate
を記述して AI の関数を呼び出す処理は、util.py で定義されている Check_solved クラスの is_strongly_solved
と is_weakly_solved_r
メソッドで記述されているので、それぞれを下記のプログラムのように修正します。
-
7 行目:AI の関数呼び出しのキーワード引数
candidate=True
をanalyze=True
に修正する。また、返り値が dict になった ので、candidate
にはその dict の 候補手の一覧を表すcandidate
のキーの値を代入 するように修正する
1 from util import Check_solved, load_bestmoves, load_mblist
2 from tqdm import tqdm
3
4 @staticmethod
5 def is_strongly_solved(ai, params=None, consider_samedata=True):
元と同じなので省略
6 for mb in tqdm(mblist):
7 candidate = set(ai(mb, analyze=True, **params)["candidate"])
8 bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
9 if candidate <= bestmoves:
10 count += 1
11 else:
12 incorrectlist.append((mb, candidate, bestmoves))
元と同じなので省略
13
14 Check_solved.is_strongly_solved = is_strongly_solved
行番号のないプログラム
from util import Check_solved, load_bestmoves, load_mblist
from tqdm import tqdm
@staticmethod
def is_strongly_solved(ai, params=None, consider_samedata=True):
if Check_solved.bestmoves_by_board is None:
Check_solved.bestmoves_by_board = load_bestmoves ("../data/bestmoves_by_board.dat")
if consider_samedata:
if Check_solved.mblist_by_board_min is None:
Check_solved.mblist_by_board_min = load_mblist("../data/mblist_by_board_min.dat")
mblist = Check_solved.mblist_by_board_min
else:
if Check_solved.mblist_by_board2 is None:
Check_solved.mblist_by_board2 = load_mblist("../data/mblist_by_board2.dat")
mblist = Check_solved.mblist_by_board2
if params is None:
params = {}
count = 0
incorrectlist = []
for mb in tqdm(mblist):
candidate = set(ai(mb, analyze=True, **params)["candidate"])
bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
if candidate <= bestmoves:
count += 1
else:
incorrectlist.append((mb, candidate, bestmoves))
nodenum = len(mblist)
print(f"{count}/{nodenum} {count/nodenum*100:.2f}%")
return count == nodenum, incorrectlist
Check_solved.is_strongly_solved = is_strongly_solved
修正箇所
from util import Check_solved, load_bestmoves, load_mblist
from tqdm import tqdm
@staticmethod
def is_strongly_solved(ai, params=None, consider_samedata=True):
元と同じなので省略
for mb in tqdm(mblist):
- candidate = set(ai(mb, candidate=True, **params))
+ candidate = set(ai(mb, analyze=True, **params)["candidate"])
bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
if candidate <= bestmoves:
count += 1
else:
incorrectlist.append((mb, candidate, bestmoves))
元と同じなので省略
Check_solved.is_strongly_solved = is_strongly_solved
-
4 行目:
is_strongly_solved
の 7 行目の修正と同じ
1 @staticmethod
2 def is_weakly_solved_r(node, ai, turn, params, registered_boards):
元と同じなので省略
3 if turn == node.mb.turn:
4 moves = ai(node.mb, analyze=True, **params)["candidate"]
5 else:
6 moves = node.mb.calc_legal_moves()
元と同じなので省略
7
8 Check_solved.is_weakly_solved_r = is_weakly_solved_r
行番号のないプログラム
@staticmethod
def is_weakly_solved_r(node, ai, turn, params, registered_boards):
txt = node.mb.board_to_str()
if txt in registered_boards:
return True
Check_solved.count += 1
registered_boards.add(txt)
if node.mb.status == turn or node.mb.status == Marubatsu.DRAW:
return True
elif node.mb.status != Marubatsu.PLAYING:
return False
if turn == node.mb.turn:
moves = ai(node.mb, analyze=True, **params)["candidate"]
else:
moves = node.mb.calc_legal_moves()
for move in moves:
childnode = node.children_by_move[move]
if not Check_solved.is_weakly_solved_r(childnode, ai, turn, params, registered_boards):
return False
return True
Check_solved.is_weakly_solved_r = is_weakly_solved_r
修正箇所
@staticmethod
def is_weakly_solved_r(node, ai, turn, params, registered_boards):
元と同じなので省略
if turn == node.mb.turn:
- moves = ai(node.mb, candidate=True, **params)
+ moves = ai(node.mb, analyze=True, **params)["candidate"]
else:
moves = node.mb.calc_legal_moves()
元と同じなので省略
Check_solved.is_weakly_solved_r = is_weakly_solved_r
上記の修正後に、下記のプログラムを実行して is_strongly_solved
と is_weakly_solved
を使って ai3s
を判定 すると、強解決でも、弱解決でもないという、正しい判定が行われることが確認できます。また、本記事での記述は省略しますが、ai3
を判定しても同様の結果が得られるので、興味がある方は確認して下さい。
Check_solved.is_strongly_solved(ai3s)
実行結果
100%|██████████| 431/431 [00:00<00:00, 3315.25it/s]
117/431 27.15%
(False,
[(<marubatsu.Marubatsu at 0x286603756d0>, {(1, 2), (2, 1), (2, 2)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x286603759d0>, {(1, 2), (2, 2)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x28660375cd0>, {(2, 1), (2, 2)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x28660375fd0>, {(0, 2), (1, 2), (2, 1)}, {(0, 2)}),
(<marubatsu.Marubatsu at 0x286603762d0>, {(0, 2), (1, 2)}, {(0, 2)}),
(<marubatsu.Marubatsu at 0x286603765d0>, {(0, 2), (2, 1)}, {(0, 2)}),
(<marubatsu.Marubatsu at 0x286603768d0>, {(1, 1)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x28660376bd0>, {(0, 2), (1, 2), (2, 2)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x28660376e90>, {(1, 2), (2, 2)}, {(1, 2)}),
(<marubatsu.Marubatsu at 0x28660377190>, {(0, 2), (2, 2)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x28660377490>, {(1, 1)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x28660377790>, {(1, 1)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x286603792d0>, {(0, 2), (2, 1), (2, 2)}, {(2, 1)}),
(<marubatsu.Marubatsu at 0x28660379590>, {(0, 2), (2, 1)}, {(2, 1)}),
(<marubatsu.Marubatsu at 0x28660379b90>, {(1, 1)}, {(2, 2)}),
(<marubatsu.Marubatsu at 0x28660379e90>, {(0, 2), (1, 2), (2, 1)}, {(2, 1)}),
(<marubatsu.Marubatsu at 0x2866037a4d0>,
{(0, 1), (0, 2), (1, 2), (2, 1), (2, 2)},
{(1, 2)}),
略
Check_solved.is_weakly_solved(ai3s)
実行結果
o False
x False
Both False
False
残りの AI の関数の修正について
AI の関数はこれまでに 20 種類以上作成した ので、上記の修正を行うのは大変です。そこで、残りの AI の関数の修正は次回の記事で行うことにして、ゲーム盤のマスに そのマスに着手を行った局面に対する ai3s
の評価値を表示する処理 を実装することにします。
なお、gui_play
で〇×ゲームを GUI で遊ぶ際に、キーワード引数 candidate
を記述して AI の関数を呼び出す処理を行っていない ので、下記のプログラムで gui_play
を実行しても問題なく動作します。なお、実行結果はこれまでと同様なので省略します。
from util import gui_play
gui_play()
AI が計算した評価値または候補手の表示の実装
合法手を着手した局面の AI の評価値の表示を常に表示すると真剣勝負の邪魔になるので、前回の記事で作成した「状況」ボタンによってその表示を切り替えることにします。
また、具体的な表示は以下のようにすることにします。
- 評価値を計算する AI の場合は マスの中 に AI が計算した 評価値を表示 する。また、そのマスが AI の 候補手に含まれている場合は赤字 で、含まれていない場合は黒字 で表示する
- 評価値を計算しない AI の場合 はマスの中に、そのマスが AI の候補手に含まれている場合は「候補手」と表示 し、そうでない場合は何も表示しない
- 上記の表示は前回の記事で表示した「そのマスに着手を行った場合の局面の状況を表す文字」の下に表示する
また、上記の処理は、前回の記事で Marubatsu_GUI クラスの update_gui
の中に記述した、「そのマスに着手を行った場合の局面の状況を表す文字を表示する処理」の後に記述することにします。
下記は、そのように update_gui
を修正したプログラムです。
-
5 行目:marubatsu.py を修正した際に循環インポートが発生しないようにここで
ai3s
をローカルにインポートしている -
7 ~ 9 行目:キーワード引数
analyze=True
を記述して現在の局面に対してai3s
を呼び出し、その返り値から「それぞれの合法手を着手した場合に計算される評価値」と「候補手の一覧」を表すデータを取り出してscore_by_move
とcandidate
に代入する -
18 ~ 20 行目:
score_by_move
がNone
ではない場合は、19 行目でmove
が候補手に含まれているかどうかで文字の色を計算し、20 行目でmove
に着手した場合の評価値を表示する。評価値を表示する y 座標の値は試行錯誤して決めたものである -
21、22 行目:
score_by_move
がNone
であり、なおかつmove
が候補手に含まれている場合は、「候補手」と表示する。なお、文字の大きさを5*self.size
で計算して表示すると「候補手」の文字の右端がゲーム盤の枠に重なってしまうので小さくした
1 from marubatsu import Marubatsu_GUI
2
3 def update_gui(self):
元と同じなので省略
4 if self.show_status:
5 from ai import ai3s
6 bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
7 analyze = ai3s(self.mb, analyze=True)
8 score_by_move = analyze["score_by_move"]
9 candidate = analyze["candidate"]
10 for move in self.mb.calc_legal_moves():
11 x, y = move
12 mb = deepcopy(self.mb)
13 mb.move(x, y)
14 score = self.score_table[mb.board_to_str()]["score"]
15 color = "red" if move in bestmoves else "black"
16 text = calc_status_txt(score)
17 ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
18 if score_by_move is not None:
19 color = "red" if move in candidate else "black"
20 ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
21 elif move in candidate:
22 ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
23
24 Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
from marubatsu import Marubatsu_GUI
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(0, -0.2, text, fontsize=7*self.size)
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"]
analyze = ai3s(self.mb, analyze=True)
score_by_move = analyze["score_by_move"]
candidate = analyze["candidate"]
for move in self.mb.calc_legal_moves():
x, y = move
mb = deepcopy(self.mb)
mb.move(x, y)
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 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
修正箇所
from marubatsu import Marubatsu_GUI
def update_gui(self):
元と同じなので省略
if self.show_status:
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
+ analyze = ai3s(self.mb, analyze=True)
+ score_by_move = analyze["score_by_move"]
+ candidate = analyze["candidate"]
- for x, y in self.mb.calc_legal_moves():
+ for move in self.mb.calc_legal_moves():
+ x, y = move
mb = deepcopy(self.mb)
mb.move(x, y)
score = self.score_table[mb.board_to_str()]["score"]
- color = "red" if (x, y) in bestmoves else "black"
+ 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 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)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
なお、元のプログラムでは 10 行目 を for x, y in self.mb.calc_legal_moves():
と記述していましたが、着手を表すデータ を x 座標と y 座標にわけずに そのまま記述する必要がある処理 が 15、19、20、21 行目にあるため、上記の 10、11 行目のように修正しました。この修正を行うことで、例えば 15 行目で着手を記述する処理を元の color = "red" if (x, y) in bestmoves else "black"
から、color = "red" if move in bestmoves else "black"
のように記述することができます。
上記の修正後に下記のプログラムで gui_play()
を実行して「分析」ボタンをクリックすると、実行結果の左図のように ai3s
の候補手となる真ん中のマスに赤字の 1 が、それ以外のマスに黒字の 0 という ai3s
が計算した評価値が表示される ようになります。また、真ん中のマスに着手すると、全てのマスが ai3s
の候補手になるので、実行結果の右図のように全てのマスに赤字の 0 が表示されます。
gui_play()
実行結果
また、下記のプログラムの 3、4 行目のように、ai3
で計算した評価値を表示 するように update_gui
を修正した後で、gui_play()
を実行して「分析」ボタンをクリックすると、実行結果のように ai3
が計算した候補手のマスに、候補手という文字が表示される ことが確認できます。
1 def update_gui(self):
元と同じなので省略
2 if self.show_status:
3 from ai import ai3
4 bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
5 analyze = ai3(self.mb, analyze=True)
6 score_by_move = analyze["score_by_move"]
7 candidate = analyze["candidate"]
元と同じなので省略
8
9 Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
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(0, -0.2, text, fontsize=7*self.size)
self.draw_board(ax, self.mb, lw=0.7*self.size)
if self.show_status:
from ai import ai3
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
analyze = ai3(self.mb, analyze=True)
score_by_move = analyze["score_by_move"]
candidate = analyze["candidate"]
for move in self.mb.calc_legal_moves():
x, y = move
mb = deepcopy(self.mb)
mb.move(x, y)
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 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 update_gui(self):
元と同じなので省略
if self.show_status:
- from ai import ai3s
- from ai import ai3
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
- analyze = ai3s(self.mb, analyze=True)
+ analyze = ai3(self.mb, analyze=True)
score_by_move = analyze["score_by_move"]
candidate = analyze["candidate"]
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
gui_play()
実行結果
今回の記事のまとめ
今回の記事では、ai3
と ai3s
が、候補手の一覧と、それぞれの合法手を着手した場合に計算される評価値を返すことができるように修正し、ゲーム盤のマスに、そのマスに着手を行った際の ai3
の候補手や、ai3s
の評価値を表示できるように修正しました。
現状では特定の AI の関数が計算した候補手または評価値しか表示できないので、次回の記事では他の AI の関数を修正し、マスに表示する評価値を計算する AI を切り替えることができるようにします。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu_new.py | 今回の記事で更新した marubatsu.py |
ai_new.py | 今回の記事で更新した ai.py |
util_new.py | 今回の記事で更新した util.py |
次回の記事