目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
再定義した AI の検証
前回の記事では、gui_play
を利用して 再定義した AI を検証 すると説明しましたが、よく考えると ai_match
を使ったほうがより簡単に検証できることに気づきましたので、その方法で検証することにします。具体的には 再定義したそれぞれの AI と ai2s
の間で対戦を行い、対戦結果を修正前の対戦結果と比較 して、ほぼ同じであれば再定義した AI が正しい処理を行うことが高いことが検証できます。
ai1
~ ai7
の検証
まず、下記のプログラムで ai1
~ ai7
を検証 します。
- 1 行目:ai モジュールをインポートする
-
4 行目:
i
を1
から7
まで 1 ずつ増やしながら繰り返す -
5 行目:組み込み関数
getattr
を利用して、1 行目でインポートした ai モジュールから、f"ai{i}"
という名前の AI の関数を取得して、ai_i
に代入する -
6 行目:
ai_match
を利用して、ai_i
とai2s
との対戦を行う。処理時間を短くするために、対戦回数を 1000 回とした
1 import ai
2 from ai import ai_match
3
4 for i in range(1, 8):
5 ai_i = getattr(ai, f"ai{i}")
6 ai_match(ai=[ai_i, ai.ai2s], match_num=1000)
行番号のないプログラム
import ai
from ai import ai_match
for i in range(1, 8):
ai_i = getattr(ai, f"ai{i}")
ai_match(ai=[ai_i, ai.ai2s], match_num=1000)
実行結果(実行結果はランダムなので下記と異なる場合があります)
ai1 VS ai2s
100%|██████████| 1000/1000 [00:03<00:00, 312.33it/s]
count win lose draw
o 769 180 51
x 438 535 27
total 1207 715 78
ratio win lose draw
o 76.9% 18.0% 5.1%
x 43.8% 53.5% 2.7%
total 60.4% 35.8% 3.9%
ai2 VS ai2s
100%|██████████| 1000/1000 [00:04<00:00, 238.41it/s]
count win lose draw
o 595 274 131
x 276 597 127
total 871 871 258
ratio win lose draw
o 59.5% 27.4% 13.1%
x 27.6% 59.7% 12.7%
total 43.5% 43.5% 12.9%
ai3 VS ai2s
100%|██████████| 1000/1000 [00:04<00:00, 249.27it/s]
count win lose draw
o 684 207 109
x 339 511 150
total 1023 718 259
ratio win lose draw
o 68.4% 20.7% 10.9%
x 33.9% 51.1% 15.0%
total 51.1% 35.9% 13.0%
ai4 VS ai2s
100%|██████████| 1000/1000 [00:03<00:00, 282.44it/s]
count win lose draw
o 822 92 86
x 589 325 86
total 1411 417 172
ratio win lose draw
o 82.2% 9.2% 8.6%
x 58.9% 32.5% 8.6%
total 70.5% 20.8% 8.6%
ai5 VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 159.67it/s]
count win lose draw
o 787 139 74
x 526 382 92
total 1313 521 166
ratio win lose draw
o 78.7% 13.9% 7.4%
x 52.6% 38.2% 9.2%
total 65.6% 26.1% 8.3%
ai6 VS ai2s
100%|██████████| 1000/1000 [00:09<00:00, 108.07it/s]
count win lose draw
o 896 11 93
x 697 68 235
total 1593 79 328
ratio win lose draw
o 89.6% 1.1% 9.3%
x 69.7% 6.8% 23.5%
total 79.7% 4.0% 16.4%
ai7 VS ai2s
100%|██████████| 1000/1000 [00:05<00:00, 170.71it/s]
count win lose draw
o 950 2 48
x 827 30 143
total 1777 32 191
ratio win lose draw
o 95.0% 0.2% 4.8%
x 82.7% 3.0% 14.3%
total 88.8% 1.6% 9.6%
下記は 修正前 と修正後の ai2s
との対戦成績を比較した表 です。左の数値が修正前、右の数値が修正後のものです。表から すべての対戦成績 が修正前と修正後で ほとんど変わらない ので、AI の再定義が正しく行われた可能性が高い ことが確認できます。
AI | 〇 の勝率 | × の勝率 | 引き分け率 |
---|---|---|---|
ai1 |
61.4/60.4 | 34.5/35.8 | 4.1/ 3.9 |
ai2 |
43.9/43.5 | 43.7/43.5 | 12.5/12.9 |
ai3 |
54.1/51.1 | 33.4/35.9 | 12.5/13.0 |
ai4 |
70.1/70.5 | 21.3/20.8 | 8.6/ 8.6 |
ai5 |
66.5/65.6 | 26.0/26.1 | 7.4/ 8.3 |
ai6 |
79.6/79.7 | 4.2/ 4.0 | 16.2/16.4 |
ai7 |
89.0/88.8 | 1.3/ 1.6 | 9.7/ 9.6 |
ai1s
~ ai14s
の検証
次に、下記のプログラムで ai1s
~ ai14s
を検証します。プログラムは先ほどとほぼ同じなので説明は省略します。
for i in range(1, 15):
ai_is = getattr(ai, f"ai{i}s")
ai_match(ai=[ai_is, ai.ai2s], match_num=1000)
修正箇所
-for i in range(1, 8):
+for i in range(1, 15):
- ai_i = getattr(ai, f"ai{i}")
+ ai_is = getattr(ai, f"ai{i}s")
- ai_match(ai=[ai_i, ai.ai2s], match_num=1000)
+ ai_match(ai=[ai_is, ai.ai2s], match_num=1000)
実行結果(実行結果はランダムなので下記と異なる場合があります)
ai1s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 164.07it/s]
count win lose draw
o 764 190 46
x 440 526 34
total 1204 716 80
ratio win lose draw
o 76.4% 19.0% 4.6%
x 44.0% 52.6% 3.4%
total 60.2% 35.8% 4.0%
ai2s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 147.85it/s]
count win lose draw
o 567 300 133
x 311 559 130
total 878 859 263
ratio win lose draw
o 56.7% 30.0% 13.3%
x 31.1% 55.9% 13.0%
total 43.9% 43.0% 13.2%
ai3s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 144.43it/s]
count win lose draw
o 685 182 133
x 412 471 117
total 1097 653 250
ratio win lose draw
o 68.5% 18.2% 13.3%
x 41.2% 47.1% 11.7%
total 54.9% 32.6% 12.5%
ai4s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 152.51it/s]
count win lose draw
o 832 97 71
x 564 334 102
total 1396 431 173
ratio win lose draw
o 83.2% 9.7% 7.1%
x 56.4% 33.4% 10.2%
total 69.8% 21.6% 8.6%
ai5s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 150.70it/s]
count win lose draw
o 801 131 68
x 535 381 84
total 1336 512 152
ratio win lose draw
o 80.1% 13.1% 6.8%
x 53.5% 38.1% 8.4%
total 66.8% 25.6% 7.6%
ai6s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 135.45it/s]
count win lose draw
o 901 11 88
x 690 85 225
total 1591 96 313
ratio win lose draw
o 90.1% 1.1% 8.8%
x 69.0% 8.5% 22.5%
total 79.5% 4.8% 15.7%
ai7s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 148.10it/s]
count win lose draw
o 967 3 30
x 805 34 161
total 1772 37 191
ratio win lose draw
o 96.7% 0.3% 3.0%
x 80.5% 3.4% 16.1%
total 88.6% 1.8% 9.6%
ai8s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 140.75it/s]
count win lose draw
o 982 1 17
x 885 26 89
total 1867 27 106
ratio win lose draw
o 98.2% 0.1% 1.7%
x 88.5% 2.6% 8.9%
total 93.3% 1.4% 5.3%
ai9s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 132.77it/s]
count win lose draw
o 993 1 6
x 874 30 96
total 1867 31 102
ratio win lose draw
o 99.3% 0.1% 0.6%
x 87.4% 3.0% 9.6%
total 93.3% 1.6% 5.1%
ai10s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 137.10it/s]
count win lose draw
o 974 0 26
x 860 28 112
total 1834 28 138
ratio win lose draw
o 97.4% 0.0% 2.6%
x 86.0% 2.8% 11.2%
total 91.7% 1.4% 6.9%
ai11s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 142.71it/s]
count win lose draw
o 992 0 8
x 875 6 119
total 1867 6 127
ratio win lose draw
o 99.2% 0.0% 0.8%
x 87.5% 0.6% 11.9%
total 93.3% 0.3% 6.3%
ai12s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 147.33it/s]
count win lose draw
o 986 0 14
x 899 0 101
total 1885 0 115
ratio win lose draw
o 98.6% 0.0% 1.4%
x 89.9% 0.0% 10.1%
total 94.2% 0.0% 5.8%
ai13s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 133.52it/s]
count win lose draw
o 990 0 10
x 883 0 117
total 1873 0 127
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%
ai14s VS ai2s
100%|██████████| 1000/1000 [00:08<00:00, 124.52it/s]
count win lose draw
o 989 0 11
x 891 0 109
total 1880 0 120
ratio win lose draw
o 98.9% 0.0% 1.1%
x 89.1% 0.0% 10.9%
total 94.0% 0.0% 6.0%
下記は 修正前 と修正後の ai2s
との対戦成績を比較した表です。左の数値が修正前、右の数値が修正後のものです。表からすべての対戦成績が修正前と修正後でほとんど変わらないので、AI の再定義が正しく行われた可能性が高い ことが確認できます。
AI | 〇 の勝率 | × の勝率 | 引き分け率 |
---|---|---|---|
ai1s |
61.4/60.2 | 34.5/35.8 | 4.1/ 4.0 |
ai2s |
43.9/44.5 | 43.9/43.0 | 12.5/13.2 |
ai3s |
54.1/54.9 | 33.4/32.6 | 12.5/12.5 |
ai4s |
70.1/69.8 | 21.3/21.6 | 8.6/ 8.6 |
ai5s |
66.5/66.8 | 26.0/25.6 | 7.4/ 7.6 |
ai6s |
79.6/79.5 | 4.2/ 4.8 | 16.2/15.7 |
ai7s |
89.0/88.6 | 1.3/ 1.8 | 9.7/ 9.6 |
ai8s |
93.8/93.3 | 1.3/ 1.4 | 4.9/ 5.3 |
ai9s |
94.1/93.3 | 1.3/ 1.6 | 4.6/ 5.1 |
ai10s |
91.5/91.7 | 1.3/ 1.4 | 7.2/ 6.9 |
ai11s |
93.4/93.3 | 0.4/ 0.3 | 6.2/ 6.3 |
ai12s |
93.5/94.2 | 0.0/ 0.0 | 6.5/ 5.8 |
ai13s |
93.7/93.7 | 0.0/ 0.0 | 6.3/ 6.3 |
ai14s |
93.9/94.0 | 0.0/ 0.0 | 6.1/ 6.0 |
ai_gt1
の検証
ゲーム木を利用した強解決 の AI である ai_gt1
~ ai_gt6
は、同じゲーム木のデータを利用 して着手を選択する AI なので、ai_gt1
のみ を下記のプログラムで検証することにします。
from ai import ai_gt1
from tree import Mbtree
mbtree = Mbtree.load("../data/aidata")
ai_match(ai=[ai_gt1, ai.ai2s],
params=[{"mbtree": mbtree}, {}], match_num=1000)
実行結果
ai_gt1 VS ai2s
100%|██████████| 1000/1000 [00:03<00:00, 309.15it/s]
count win lose draw
o 961 0 39
x 784 0 216
total 1745 0 255
ratio win lose draw
o 96.1% 0.0% 3.9%
x 78.4% 0.0% 21.6%
total 87.2% 0.0% 12.8%
下記は 修正前 と修正後の ai2s
との対戦成績を比較した表です。左の数値が修正前、右の数値が修正後のものです。表から ai_gt1
の再定義が正しく行われた可能性が高い ことが確認できます。
AI | 〇 の勝率 | × の勝率 | 引き分け率 |
---|---|---|---|
ai_gt1 |
87.4/87.2 | 0.0/ 0.0 | 12.6/ 12.8 |
以上から、再定義した全ての AI が正しく行われた可能性が高いことが確認できました。
AI が計算した候補手または評価値のゲーム盤への表示
これで AI の関数をすべて再定義し、それぞれが 仮引数 analyze
を持ち、候補手の一覧 と、評価値を計算する AI の場合は それぞれの合法手を着手した際の局面の評価値のデータ を 返すことができる ようになりました。
以前の記事 で、ai3s
が計算した 評価値のゲーム盤への表示 の処理を実装を行いましたが、AI の関数を上記のようにすべて再定義したことによって、任意の AI が計算した 候補手または評価値のゲーム盤への表示 を行うことができるようになったので、その実装を行います。
Marubatsu_GUI クラスの create_dropdown
の修正
まず、候補手などを計算する AI を選択するための Dropdown を作成 し、画面に配置 する必要があります。その Dropdown に登録する AI は、対戦を行う AI と同じ ものとし、下記のプログラムのように Marubatsu_GUI クラスの create_dropdown
を修正して Dropdown を作成し、status_dropdown
という属性に代入することにします。
-
18 ~ 23 行目:9 ~ 15 行の 〇 と × を担当する AI と同じ項目を持つ Dropdown を作成し、
status_dropdown
属性に代入する。ただし、9 ~ 15 行目にあった、Dropdown の左に表示する説明文を表すdescription
は必要ないので削除した
1 from marubatsu import Marubatsu_GUI
2 import ipywidgets as widgets
3
4 def create_dropdown(self):
元と同じなので省略
5 for i in range(2):
6 # Dropdown の description を計算する
7 description = "〇" if i == 0 else "×"
8 self.dropdown_list.append(
9 widgets.Dropdown(
10 options=self.ai_dict,
11 description=description,
12 layout=widgets.Layout(width="100px"),
13 style={"description_width": "20px"},
14 value=select_values[i],
15 )
16 )
17
18 self.status_dropdown = widgets.Dropdown(
19 options=self.ai_dict,
20 layout=widgets.Layout(width="100px"),
21 style={"description_width": "20px"},
22 value=select_values[0],
23 )
24
25 Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets
def create_dropdown(self):
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
self.dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
value = ( self.mb.ai[i], self.params[i] )
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in self.ai_dict.values():
# 項目を登録する
self.ai_dict[self.names[i]] = value
for i in range(2):
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
self.dropdown_list.append(
widgets.Dropdown(
options=self.ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
self.status_dropdown = widgets.Dropdown(
options=self.ai_dict,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets
def create_dropdown(self):
元と同じなので省略
for i in range(2):
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
self.dropdown_list.append(
widgets.Dropdown(
options=self.ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
+ self.status_dropdown = widgets.Dropdown(
+ options=self.ai_dict,
+ layout=widgets.Layout(width="100px"),
+ style={"description_width": "20px"},
+ value=select_values[0],
+ )
Marubatsu_GUI.create_dropdown = create_dropdown
Marubatsu_GUI クラスの display_widgets
の修正
次に、display_widgets
を下記のプログラムの 3 行目のように修正して、上記で作成した Dropdown を「状況」ボタンの右に配置 することにします。
1 def display_widgets(self):
元と同じなので省略
2 # 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
3 hbox2 = widgets.HBox([self.show_status_button, self.status_dropdown, self.size_slider])
元と同じなので省略
4
5 Marubatsu_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):
# 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button,
self.show_tree_button, self.reset_tree_button, self.help_button])
# 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
hbox2 = widgets.HBox([self.show_status_button, self.status_dropdown, self.size_slider])
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider])
# hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help]))
Marubatsu_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
元と同じなので省略
# 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
- hbox2 = widgets.HBox([self.show_status_button, self.size_slider])
+ hbox2 = widgets.HBox([self.show_status_button, self.status_dropdown, self.size_slider])
元と同じなので省略
Marubatsu_GUI.display_widgets = display_widgets
Marubatsu_GUI クラスの update_gui
の修正
次に、update_gui
を下記のプログラムのように修正して、Dropdown で選択した AI が計算した評価値または候補手を表示 するようにします。なお、修正の際に、上部に表示される 局面の状況 に 〇、×、△ だけを表示するとわかりづらい気がしましたので、「状況 〇」 のように マークの前に「状況」という文字を表示 するように修正しました。
- 15 行目:局面の状況を表示する際に、「状況」という文字を表示するように修正した
- 16 行目:「状況」という文字を リプレイモードで表示 すると、「Replay」という文字が右にはみ出してしまう ようになるので、7、8 行目と同様の方法で 中央揃えで表示 することで、はみ出さないようにした
-
19 行目:Dropdown が選択中の項目の値から AI の関数 と、AI のパラメーター を取り出して
ai
とparams
に代入 する -
20 行目:Dropdown に 「人間」の項目が選択 されている場合は、
ai
にNone
が代入 されているので、ai
にNone
が代入されていない場合に処理を行うようにする。この処理を記述し忘れると、人間の項目が選択される場合に 21 行目の処理でエラーが発生する -
21 ~ 23 行目:
ai
とparams
を利用してscore_by_move
とcandidate
を計算するように修正する -
25 ~ 30 行目:
ai
がNone
でない場合に評価値または候補手を表示するように修正する
1 from marubatsu import Marubatsu
2 from copy import deepcopy
3
4 def update_gui(self):
元と同じなので省略
5 # 上部のメッセージを描画する
6 # 対戦カードの文字列を計算する
7 ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
8 fontsize=7*self.size, ha="center")
9
10 # ゲームの決着がついていない場合は、手番を表示する
11 if self.mb.status == Marubatsu.PLAYING:
12 text = "Turn " + self.mb.turn
13 score = self.score_table[self.mb.board_to_str()]["score"]
14 if self.show_status:
15 text += " 状況 " + calc_status_txt(score)
元と同じなので省略
16 ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")
元と同じなので省略
17 if self.show_status:
18 bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
19 ai, params = self.status_dropdown.value
20 if ai is not None:
21 analyze = ai(self.mb, analyze=True, **params)
22 score_by_move = analyze["score_by_move"]
23 candidate = analyze["candidate"]
24 for move in self.mb.calc_legal_moves():
元と同じなので省略
25 if ai is not None:
26 if score_by_move is not None:
27 color = "red" if move in candidate else "black"
28 ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
29 elif move in candidate:
30 ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
31
32 Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
from marubatsu import Marubatsu
from copy import deepcopy
def update_gui(self):
def calc_status_txt(score):
if score > 0:
return "〇"
elif score == 0:
return "△"
else:
return "×"
ax = self.ax
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# リプレイ中、ゲームの決着がついていた場合は背景色を変更する
is_replay = self.mb.move_count < len(self.mb.records) - 1
if self.mb.status == Marubatsu.PLAYING:
facecolor = "lightcyan" if is_replay else "white"
else:
facecolor = "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
fontsize=7*self.size, ha="center")
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
score = self.score_table[self.mb.board_to_str()]["score"]
if self.show_status:
text += " 状況 " + calc_status_txt(score)
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
# リプレイ中の場合は "Replay" を表示する
if is_replay:
text += " Replay"
ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")
self.draw_board(ax, self.mb, lw=0.7*self.size)
if self.show_status:
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
ai, params = self.status_dropdown.value
if ai is not None:
analyze = ai(self.mb, analyze=True, **params)
score_by_move = analyze["score_by_move"]
candidate = analyze["candidate"]
for move in self.mb.calc_legal_moves():
x, y = move
mb = deepcopy(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 ai is not None:
if score_by_move is not None:
color = "red" if move in candidate else "black"
ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
elif move in candidate:
ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
from tree import Node
self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
修正箇所
from marubatsu import Marubatsu
from copy import deepcopy
def update_gui(self):
元と同じなので省略
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
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)
+ text += " 状況 " + calc_status_txt(score)
元と同じなので省略
- ax.text(0, -0.2, text, fontsize=7*self.size)
+ ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")
元と同じなので省略
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"]
+ ai, params = self.status_dropdown.value
+ if ai is not None:
+ analyze = ai(self.mb, analyze=True, **params)
+ score_by_move = analyze["score_by_move"]
+ candidate = analyze["candidate"]
for move in self.mb.calc_legal_moves():
元と同じなので省略
- 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)
+ if ai is not None:
+ if score_by_move is not None:
+ color = "red" if move in candidate else "black"
+ ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
+ elif move in candidate:
+ ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
Marubatsu_GUI クラスの create_event_handler
の修正
上記の修正で、AI の評価値または候補手が表示されるようになりますが、Dropdown の項目を変更した際 に 表示が更新されない という問題があります。そこで、create_event_handler
を下記のプログラムのように修正します。
-
8、9 行目:Dropdown が変更 された際に呼び出される イベントハンドラを定義 する。行う処理 は、
update_gui
を呼び出して、ゲーム盤の表示を更新する処理 である - 18 行目:Dropdown とイベントハンドラを結び付ける
1 import math
2
3 def create_event_handler(self):
元と同じなので省略
4 def on_show_status_button_clicked(b=None):
5 self.show_status = not self.show_status
6 self.update_gui()
7
8 def on_status_dropdown_changed(changed):
9 self.update_gui()
10
11 def on_size_slider_changed(changed):
12 self.size = changed["new"]
13 self.fig.set_figwidth(self.size)
14 self.fig.set_figheight(self.size)
15 self.update_gui()
16
17 self.show_status_button.on_click(on_show_status_button_clicked)
18 self.status_dropdown.observe(on_status_dropdown_changed, names="value")
19 self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
20
21 Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
import math
def create_event_handler(self):
# 乱数の種のチェックボックスのイベントハンドラを定義する
def on_checkbox_changed(changed):
self.update_widgets_status()
self.checkbox.observe(on_checkbox_changed, names="value")
# 開く、保存ボタンのイベントハンドラを定義する
def on_load_button_clicked(b=None):
path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
initialdir="save")
if path != "":
with open(path, "rb") as f:
data = pickle.load(f)
self.mb.records = data["records"]
self.mb.ai = data["ai"]
self.params = data["params"] if "params" in data else [ {}, {} ]
if "names" in data:
names = data["names"]
else:
names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]
options = self.dropdown_list[0].options.copy()
for i in range(2):
value = (self.mb.ai[i], self.params[i])
if not value in options.values():
options[names[i]] = value
for i in range(2):
self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
if data["seed"] is not None:
self.checkbox.value = True
self.inttext.value = data["seed"]
else:
self.checkbox.value = False
def on_save_button_clicked(b=None):
names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
fname = f"{names[0]} VS {names[1]} {timestr}"
path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
initialdir="save", initialfile=fname,
defaultextension="mbsav")
if path != "":
with open(path, "wb") as f:
data = {
"records": self.mb.records,
"move_count": self.mb.move_count,
"ai": self.mb.ai,
"params": self.params,
"names": names,
"seed": self.inttext.value if self.checkbox.value else None
}
pickle.dump(data, f)
def on_show_tree_button_clicked(b=None):
self.show_subtree = not self.show_subtree
self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
self.update_gui()
def on_reset_tree_button_clicked(b=None):
self.update_gui()
def on_help_button_clicked(b=None):
self.help.layout.display = "none" if self.help.layout.display is None else None
self.load_button.on_click(on_load_button_clicked)
self.save_button.on_click(on_save_button_clicked)
self.show_tree_button.on_click(on_show_tree_button_clicked)
self.reset_tree_button.on_click(on_reset_tree_button_clicked)
self.help_button.on_click(on_help_button_clicked)
def on_show_status_button_clicked(b=None):
self.show_status = not self.show_status
self.update_gui()
def on_status_dropdown_changed(changed):
self.update_gui()
def on_size_slider_changed(changed):
self.size = changed["new"]
self.fig.set_figwidth(self.size)
self.fig.set_figheight(self.size)
self.update_gui()
self.show_status_button.on_click(on_show_status_button_clicked)
self.status_dropdown.observe(on_status_dropdown_changed, names="value")
self.size_slider.observe(on_size_slider_changed, names="value")
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
self.mb.play_loop(self, self.params)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b=None):
# 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
if self.checkbox.value:
random.seed(self.inttext.value)
self.mb.restart()
self.output.clear_output()
on_change_button_clicked(b)
# 待ったボタンのイベントハンドラを定義する
def on_undo_button_clicked(b=None):
if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
self.mb.move_count -= 2
self.mb.records = self.mb.records[0:self.mb.move_count+1]
self.mb.change_step(self.mb.move_count)
self.update_gui()
# イベントハンドラをボタンに結びつける
self.change_button.on_click(on_change_button_clicked)
self.reset_button.on_click(on_reset_button_clicked)
self.undo_button.on_click(on_undo_button_clicked)
# step 手目の局面に移動する
def change_step(step):
self.mb.change_step(step)
# 描画を更新する
self.update_gui()
def on_first_button_clicked(b=None):
change_step(0)
def on_prev_button_clicked(b=None):
change_step(self.mb.move_count - 1)
def on_next_button_clicked(b=None):
change_step(self.mb.move_count + 1)
def on_last_button_clicked(b=None):
change_step(len(self.mb.records) - 1)
def on_slider_changed(changed):
if self.mb.move_count != changed["new"]:
change_step(changed["new"])
self.first_button.on_click(on_first_button_clicked)
self.prev_button.on_click(on_prev_button_clicked)
self.next_button.on_click(on_next_button_clicked)
self.last_button.on_click(on_last_button_clicked)
self.slider.observe(on_slider_changed, names="value")
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.mb.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
with self.output:
self.mb.move(x, y)
# 次の手番の処理を行うメソッドを呼び出す
self.mb.play_loop(self, self.params)
# ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
def on_key_press(event):
keymap = {
"up": on_first_button_clicked,
"left": on_prev_button_clicked,
"right": on_next_button_clicked,
"down": on_last_button_clicked,
"0": on_undo_button_clicked,
"enter": on_reset_button_clicked,
"-": on_load_button_clicked,
"l": on_load_button_clicked,
"+": on_save_button_clicked,
"s": on_save_button_clicked,
"*": on_help_button_clicked,
"h": on_help_button_clicked,
}
if event.key in keymap:
keymap[event.key]()
else:
try:
num = int(event.key) - 1
event.inaxes = True
event.xdata = num % 3
event.ydata = 2 - (num // 3)
on_mouse_down(event)
except:
pass
# fig の画像イベントハンドラを結び付ける
self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
self.fig.canvas.mpl_connect("key_press_event", on_key_press)
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
import math
def create_event_handler(self):
元と同じなので省略
def on_show_status_button_clicked(b=None):
self.show_status = not self.show_status
self.update_gui()
+ def on_status_dropdown_changed(changed):
+ self.update_gui()
def on_size_slider_changed(changed):
self.size = changed["new"]
self.fig.set_figwidth(self.size)
self.fig.set_figheight(self.size)
self.update_gui()
self.show_status_button.on_click(on_show_status_button_clicked)
+ self.status_dropdown.observe(on_status_dropdown_changed, names="value")
self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
gui_play
の検証
上記の修正後に、下記のプログラムで gui_play
を実行し、状況ボタンの右の Dropdown に ai1s
を選択 して 「状況」ボタンをクリック すると、実行結果のようにそれぞれのマスに ai1s
が計算した評価値 が、前回の記事 で 再定義した ai1s
が計算した値と同じものが表示 されることが確認できます。また、最も評価値の高い (0, 0) のマスの 評価値の色 が ai1s
の候補手 として 赤色で表示される ことが確認できます。また、今回の記事の修正で、ゲーム盤の上部に「状況」という文字が表示されるようになったことも確認できます。
from util import gui_play
gui_play()
実行結果
また、Dropdown に ai14s
、ai_gt6
、人間
を選択した場合は、左から順に下図のようになります。ai14s
は真ん中の (1, 1) を候補手としますが、その理由 が評価値が表示されることによって わかりやすくなります。また、この機能を実装したことで、新しく AI を実装した際に、AI が計算した評価値を容易に確認できる ようになるというメリットが得られます。
ai_gt6
は評価値を計算しないので、候補手という文字が、人間は AI ではないので何も表示されなくなります。
また、リプレイモードにした場合に、下図のようにゲーム盤の上部の文字が中央揃えで表示されることで、はみでないことが確認できます。
今回の記事の内容
今回の記事では、再定義した AI の関数の検証を行い、任意の AI が計算した候補手または評価値のゲーム盤への表示を行えるようにしました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu.py | 本記事で更新した marubatsu_new.py |
次回の記事
近日公開予定です