0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その133 再定義した AI の検証と任意の AI が計算した候補手または評価値のゲーム盤への表示

Posted at

目次と前回の記事

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

リンク 説明
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 行目i1 から 7 まで 1 ずつ増やしながら繰り返す
  • 5 行目:組み込み関数 getattr を利用して、1 行目でインポートした ai モジュールから、f"ai{i}" という名前の AI の関数を取得して、ai_i に代入する
  • 6 行目ai_match を利用して、ai_iai2s との対戦を行う。処理時間を短くするために、対戦回数を 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 のパラメーター を取り出して aiparams に代入 する
  • 20 行目:Dropdown に 「人間」の項目が選択 されている場合は、aiNone が代入 されているので、aiNone が代入されていない場合に処理を行うようにする。この処理を記述し忘れると、人間の項目が選択される場合に 21 行目の処理でエラーが発生する
  • 21 ~ 23 行目aiparams を利用して score_by_movecandidate を計算するように修正する
  • 25 ~ 30 行目aiNone でない場合に評価値または候補手を表示するように修正する
 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 に ai14sai_gt6人間 を選択した場合は、左から順に下図のようになります。ai14s は真ん中の (1, 1) を候補手としますが、その理由 が評価値が表示されることによって わかりやすくなります。また、この機能を実装したことで、新しく AI を実装した際に、AI が計算した評価値を容易に確認できる ようになるというメリットが得られます。

ai_gt6 は評価値を計算しないので、候補手という文字が、人間は AI ではないので何も表示されなくなります。

  

また、リプレイモードにした場合に、下図のようにゲーム盤の上部の文字が中央揃えで表示されることで、はみでないことが確認できます。

今回の記事の内容

今回の記事では、再定義した AI の関数の検証を行い、任意の AI が計算した候補手または評価値のゲーム盤への表示を行えるようにしました。

本記事で入力したプログラム

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu.py 本記事で更新した marubatsu_new.py

次回の記事

近日公開予定です

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?