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を一から作成する その118 最善手の優劣を考慮したゲーム木を利用する AI

Last updated at Posted at 2024-09-22

目次と前回の記事

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

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

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
test.py テストに関する関数
util.py ユーティリティ関数の定義。現在は gui_play のみ定義されている
tree.py ゲーム木に関する Node、Mbtree クラスの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

今回の記事のテーマ

以前の記事でルールベースの AI を作成する際に、人間と対戦を行うという視点で考える と、最善手には優劣が存在する という説明をしました。今回の記事では、ゲーム木を利用した AI の場合に 最善手の優劣を考慮する方法 について紹介します。

必勝の局面の最善手の優劣

以前の記事で、必勝の局面 での 最善手 には、感情を持つ人間 にとって 好ましいと思えるかどうか という点で 違いが生じる場合がある という説明を行いました。忘れてしまった方が多いと思いますので具体例を挙げて説明します。

なお、感情を持たない弱解決または強解決の AI どうしの対戦 の場合は、どの最善手を選択しても 試合の結果は変わらない ので、必勝の局面に限らず、あらゆる局面最善手に優劣は存在しません

ゲーム木を利用した ai_gt6 は強解決の AI ですが、現状の ai_gt6 は、次の着手で勝利できる場合 に、すぐに勝利しない着手を行う場合があります。そのことは下記の手順で確認することができます。

from util import gui_play

gui_play()
  1. 上記のプログラムで gui_play() を実行する
  2. (1, 1)、(1, 0)、(0, 0) の順で着手を行う(下図)
  3. 〇 の担当を ai_gt6 に変更し、「変更」ボタンをクリックする
  4. (2, 0) に着手を行う

上記の操作を行うと、下図のように、〇を担当する ai_gt6(2, 2) に着手を行って勝利できる 局面であるにも関わらず、それ以外の着手を行う場合があります。なお、ai_gt6 が (2, 2) に着手を行って勝利する場合もあるので、その場合は「待った」ボタンをクリックして ai_gt6 が勝利する着手を行わなくなるまで (2, 0) に着手し直してください。

下図は、上記で「<」ボタンをクリックして表示される 1 手前の局面 の GUI の部分木を含んだ画面です。

上図の GUI の部分木から、1 手前の ai_gt6 の手番の局面では、(2, 2) に着手を行えばすぐに 〇 の勝利になりますが、(0, 1)、(2, 1)、(0, 2)いずれかに着手を行った場合 でも水色で表示される 〇 の必勝の局面になり、その後でお互いが最善手を着手し続けると 〇 が勝利することがわかるので、それらも最善手である ことが確認できます。

このような、すぐに勝利できる合法手が存在するにも関わらず、AI が別の合法手を着手するという現象は、ルールベースの AI を作成する際にも発生 し、そのことについては 以前の記事 で説明しました。

勝敗の結果が変わらないという意味では、すぐに勝利しない着手を AI が選択しても強解決の AI であることには変わりませんが、人間がその AI と対戦する場合は、そのような 無駄に試合を長引かせるような着手気分が良いものではありません ので、すぐに勝利することができる最善手 のほうが、人間相手に対戦を行う場合は優れている と考えることができます。そこで、必勝の局面 では、できるだけ早く勝利する着手を行う ような AI を作成することにします。そのような AI を作成する方法について少し考えてみて下さい。

評価値の計算方法の変更

ルールベースの AI の場合は、着手すると勝利する合法手の評価値 を、最も高い評価値に設定する ことで最善手に優劣をつけることができました。

ゲーム木を利用した AI では、以前の記事で説明した、下記の ミニマックス法 というアルゴリズムでゲーム木の各 ノードの評価値を計算 します。

  1. ゲームの決着がついている リーフノードの評価値を全て計算 する
  2. 全ての子ノードの評価値が求められている ノードの評価値を計算する
  3. 手順 2 を、全ての子ノードの評価値が求められているノードが なくなるまで 繰り返す

ただし、手順 2 では、以下の方法で評価値を計算する。

  • ノードが 〇 の手番の場合は、子ノードの評価値の中で最も高い評価値とする
  • ノードが × の手番の場合は、子ノードの評価値の中で最も低い評価値とする

ミニマックス法では、上記のように 決着がついている局面の評価値を元に 各ノードの 評価値の計算が行われる ので、決着がついている局面の評価値 を、決着がついた手数を表す ノードの深さで変える ことで AI が なるべく早く勝利する ような合法手を 優先的に選択する ようになります。どのように評価値を変えればよいかについて少し考えてみて下さい。

〇 の必勝の局面の評価値

必勝の局面には、〇 の必勝の局面と × の必勝の局面の 2 種類があるので、それぞれについて評価値を考えることにします。

ゲーム木を利用した AI は、〇 の手番 の局面では、最も評価値が高い子ノード になる 合法手を選択 します。そのため、これまでは 〇 が勝利 する局面の 評価値を常に 1 と計算しましたが、〇 の必勝の局面なるべく早く 〇 が勝利する着手を選択する ためには、〇 が勝利する局面の 深さが浅いほど大きな評価値を計算する 必要があります。

また、現状では 引き分けの局面 の評価値を 0× の必勝の局面 の評価値を -1 と計算しているので、〇 の必勝の局面 の評価値はそれらより大きな 正の値に設定する必要 があります。

〇×ゲームは、最低でも 3 回着手を行わなければ勝利できないゲームなので、〇が勝利する局面 は、深さが 5、7、9 のいずれかの局面 になります。この 3 つの深さに対して、上記の条件を満たす評価値を割り当てる方法は無数にありますが、下記の表のように、深い順1、2、3 という 評価値を割り当てる のが最も 自然な割り当て方 ではないかと思います。

局面の深さ 5 7 9
〇が勝利した場合の評価値 3 2 1

次に、局面の深さから 上記の表の 評価値を計算する式を考える 必要があります。どのような式で計算できるかについて少し考えてみて下さい。

上記の条件を満たしていれば、他の割り当て方でもかまわないので、他の評価値を割り当てたい人はそのようにして下さい。

その式は、「(11 - 局面の深さ)÷ 2」という式で計算できます。

× の必勝の局面の評価値

ゲーム木を利用した AI は、× の手番 の局面では、最も評価値が低い子ノード になる 合法手を選択 します。そのため × の必勝の局面なるべく早く × が勝利する着手を選択する ためには、× が勝利する局面の 深さが浅いほど大きな評価値を計算する 必要があります。

また、先程と同様の理由でその評価値は 負の値にする必要 があります。

× が勝利する局面は 深さが 6、8 のいずれかの局面なので、それらの評価値を下記の表のように割り当てることにします。

局面の深さ 6 8
× が勝利した場合の評価値 -2 -1

また、上記の評価値を計算する式は「(局面の深さ - 10)÷ 2」となります。

評価値のまとめ

下記の表は上記をまとめたものです。なお、引き分けの局面はこれまでと同様に 0 を評価値とします。

局面の深さ 5 6 7 8 9 評価値の計算式
〇 が勝利した場合の評価値 3 2 1 (11 - 局面の深さ)÷ 2
× が勝利した場合の評価値 -2 -1 (局面の深さ - 10)÷ 2

Mbtree クラスの修正

ゲーム木の 評価値の計算 は、Mbtree クラスの calc_score_by_bfcalc_score_by_df メソッドで行っているので、それらのメソッドで上記の評価値を計算するように修正する必要があります。ただし、これまでと同様の評価値を計算できる ように、__init__ メソッドに True が代入されている場合に最短(shortest)の勝利(victory)を優先した評価値を計算することを表す shortest_victory という 仮引数を追加 することにします。

__init__ メソッドの修正

まず、__init__ メソッドを下記のプログラムのように修正します。

  • 4 行目:デフォルト値を False とする仮引数 shortest_victory を追加する
  • 6 行目shortest_victory を同名の属性に代入する
1  from tree import Mbtree, Node
2  from marubatsu import Marubatsu
3
4  def __init__(self, algo="bf", shortest_victory=False):
5      self.algo = algo
6      self.shortest_victory = shortest_victory
元と同じなので省略
7    
8  Mbtree.__init__ = __init__ 
行番号のないプログラム
from tree import Mbtree, Node
from marubatsu import Marubatsu

def __init__(self, algo="bf", shortest_victory=False):
    self.algo = algo
    self.shortest_victory = shortest_victory
    Node.count = 0
    self.root = Node(Marubatsu())
    if self.algo == "bf":  
        self.create_tree_by_bf()
        self.calc_score_by_bf()
    else:
        self.nodelist = [self.root]
        self.nodelist_by_depth = [[] for _ in range(10)]
        self.nodelist_by_depth[0].append(self.root)
        self.nodenum = 0
        self.create_tree_by_df(self.root)
        self.nodelist_by_score = []
        self.calc_score_by_df(self.root)
    self.nodelist_by_mb = {}
    self.bestmoves_by_mb = {}
    self.calc_bestmoves(self.root)
    
Mbtree.__init__ = __init__ 
修正箇所
from tree import Mbtree, Node
from marubatsu import Marubatsu

-def __init__(self, algo="bf"):
+def __init__(self, algo="bf", shortest_victory=False):
    self.algo = algo
+   self.shortest_victory = shortest_victory
元と同じなので省略
    
Mbtree.__init__ = __init__ 

calc_score_of_node メソッドの定義

次に、calc_score_by_bfcalc_score_by_df メソッドの 両方 で、shortest_victoryTrue の場合の評価値を計算する処理を記述する 必要がありますが、その処理は 両方のメソッドで全く同じ です。そこで、ゲームの 決着がついたノードの評価値を計算する 下記のメソッドを定義する事にします。

名前:ノードの評価値を計算するので calc_score_of_node とする
処理:決着がついたノードの評価値を計算し、score 属性に代入する
入力:評価値を計算するノードを仮引数 node に代入する
出力:なし

下記のプログラムは、calc_score_of_node の定義です。なお、このメソッドは決着がついていないノードに対しては何も処理を行いません。

  • 1 行目:評価値を計算するノードを代入する仮引数 node を持つ calc_score_of_node メソッドを定義する
  • 2、3 行目:〇が勝利した局面の場合の nodescore 属性を、shortest_victory 属性の値に応じて計算する
  • 4、5 行目:引き分けの場合は nodescore 属性に 0 を代入する
  • 6、7 行目:×が勝利した局面の場合の nodescore 属性を、shortest_victory 属性の値に応じて計算する
1  def calc_score_of_node(self, node):
2      if node.mb.status == Marubatsu.CIRCLE:
3          node.score = (11 - node.depth) / 2 if self.shortest_victory else 1
4      elif node.mb.status == Marubatsu.DRAW:
5          node.score = 0
6      elif node.mb.status == Marubatsu.CROSS:
7          node.score = (node.depth - 10) / 2 if self.shortest_victory else -1
8        
9  Mbtree.calc_score_of_node = calc_score_of_node     
行番号のないプログラム
def calc_score_of_node(self, node):
    if node.mb.status == Marubatsu.CIRCLE:
        node.score = (11 - node.depth) / 2 if self.shortest_victory else 1
    elif node.mb.status == Marubatsu.DRAW:
        node.score = 0
    elif node.mb.status == Marubatsu.CROSS:
        node.score = (node.depth - 10) / 2 if self.shortest_victory else -1
        
Mbtree.calc_score_of_node = calc_score_of_node 

calc_score_by_bfcalc_score_by_df の修正

まず、calc_score_by_df を下記のプログラムのように修正します。

  • 5、6 行目:ゲームの決着がついている場合に、先程定義した calc_score_of_node メソッドを呼び出して node の評価値を計算するように修正する
 1  def calc_score_by_bf(self):
 2      self.nodelist_by_score = self.nodelist[:]
 3      for nodelist in reversed(self.nodelist_by_depth):
 4          for node in nodelist:
 5              if node.mb.status != Marubatsu.PLAYING:
 6                  self.calc_score_of_node(node)
 7              else:
 8                  score_list = [childnode.score for childnode in node.children]
元と同じなので省略
 9
10  Mbtree.calc_score_by_bf = calc_score_by_bf
行番号のないプログラム
def calc_score_by_bf(self):
    self.nodelist_by_score = self.nodelist[:]
    for nodelist in reversed(self.nodelist_by_depth):
        for node in nodelist:
            if node.mb.status != Marubatsu.PLAYING:
                self.calc_score_of_node(node)
            else:
                score_list = [childnode.score for childnode in node.children]
                if node.mb.move_count % 2 == 0:
                    score = max(score_list)
                else:
                    score = min(score_list)
                node.score = score
            self.nodelist_by_score.append(node)
            node.score_index = len(self.nodelist_by_score) - 1  

Mbtree.calc_score_by_bf = calc_score_by_bf
修正箇所
def calc_score_by_bf(self):
    self.nodelist_by_score = self.nodelist[:]
    for nodelist in reversed(self.nodelist_by_depth):
        for node in nodelist:
-           if node.mb.status == Marubatsu.CIRCLE:
-               node.score = 1
-           elif node.mb.status == Marubatsu.DRAW:
-               node.score = 0
-           elif node.mb.status == Marubatsu.CROSS:
-               node.score = -1
+           if node.mb.status != Marubatsu.PLAYING:
+               self.calc_score_of_node(node)
            else:
                score_list = [childnode.score for childnode in node.children]
元と同じなので省略

Mbtree.calc_score_by_bf = calc_score_by_bf

次に、calc_score_by_df を下記のプログラムのように修正します。修正内容は calc_score_by_bf の場合とほぼ同じです。

  • 3、4 行目:ゲームの決着がついている場合に、先程定義した calc_score_of_node メソッドを呼び出して node の評価値を計算するように修正する
1  def calc_score_by_df(self, node):
2      self.nodelist_by_score.append(node)
3      if node.mb.status != Marubatsu.PLAYING:
4          self.calc_score_of_node(node)
5      else:
6          score_list = []
元と同じなので省略
7
8  Mbtree.calc_score_by_df = calc_score_by_df
行番号のないプログラム
def calc_score_by_df(self, node):
    self.nodelist_by_score.append(node)
    if node.mb.status != Marubatsu.PLAYING:
        self.calc_score_of_node(node)
    else:
        score_list = []
        for childnode in node.children:
            score_list.append(self.calc_score_by_df(childnode))
            self.nodelist_by_score.append(node)
        if node.mb.move_count % 2 == 0:
            score = max(score_list)
        else:
            score = min(score_list)
        node.score = score
        
    self.nodelist_by_score.append(node)
    node.score_index = len(self.nodelist_by_score) - 1        
    return node.score      

Mbtree.calc_score_by_df = calc_score_by_df
修正箇所
def calc_score_by_df(self, node):
    self.nodelist_by_score.append(node)
-   if node.mb.status == Marubatsu.CIRCLE:
-       node.score = 1
-   elif node.mb.status == Marubatsu.DRAW:
-       node.score = 0
-   elif node.mb.status == Marubatsu.CROSS:
-       node.score = -1
+   if node.mb.status != Marubatsu.PLAYING:
+       self.calc_score_of_node(node)
    else:
        score_list = []
元と同じなので省略

Mbtree.calc_score_by_df = calc_score_by_df

ゲーム木の作成と確認

次に、下記のプログラムで 実引数に shortest_victory=True を記述して 〇×ゲームのゲーム木を 幅優先アルゴリズム で作成します。

bftree_shortest_victory = Mbtree(shortest_victory=True)

実行結果

     9 depth 1 node created
    72 depth 2 node created
   504 depth 3 node created
  3024 depth 4 node created
 15120 depth 5 node created
 54720 depth 6 node created
148176 depth 7 node created
200448 depth 8 node created
127872 depth 9 node created
     0 depth 10 node created
total node num = 549946

また、下記のプログラムで 〇×ゲームのゲーム木を 深さ優先アルゴリズム で作成します。

dftree_shortest_victory = Mbtree(algo="df", shortest_victory=True)

作成した ゲーム木が正しく作成されているかを確認 するために下記のプログラムで Mbtree_GUI クラスを使ってゲーム木の部分木を GUI で表示 すると、実行結果のように、一見すると正しい部分木が表示されるように見えますが、実際には問題 があります。どのような問題があるかについて様々なノードをクリックして探してみて下さい。

なお、今回の記事では以降は幅優先アルゴリズムで作成した bftree_shortest_victory に対してのみ確認を行います。深さ優先アルゴリズムで作成した dftree_shortest_victory に対しても同様の方法で確認を行うことができるので後で確認して下さい。

また、どちらのゲーム木も ノードの評価値は同じになる ので、後で作成する最善手の優劣を考慮した AI では、bftree_shortest_victory のほうを利用することにします。

from tree import Mbtree_GUI

Mbtree_GUI(bftree_shortest_victory)

実行結果

例えば (1, 1) のみに 〇 が配置された局面のノードをクリックすると、下図のような部分木が表示されます。下図の部分木の中で、背景が灰色で評されている部分の局面は、最善手を着手し続けた場合の局面 なので、背景が灰色の部分で 左右につながっている一連の局面すべて同じ評価値になるはず です。それにも関わらず下図では、背景が灰色の部分の中で 一番右のノードだけが水色になる局面がある 点が変です。

また、下記のプログラムでこれまでに作成したゲーム木をファイルから読み込み、同じ局面を選択すると、実行結果の左図のように 背景が灰色の部分の左右につながっている一連の局面の色がすべて同である 正しい部分木が表示されます。上図を下の実行結果の右に並べて表示しましたので比較して下さい。

mbtree = Mbtree.load("../data/aidata")
Mbtree_GUI(mbtree)

実行結果(左が実行結果です。右は上図の再掲です)

 

bftree_shortest_victory を Mbtree_GUI で表示すると、上記の左図のように間違った色で局面が表示される理由について少し考えてみて下さい。

評価値の表示

間違った色で表示される原因として、ゲーム木の評価値が正しく計算されていない可能性 が考えられます。これまでは、評価値が -1、0、1 の 3 種類しかなかったので、ゲーム盤の色で評価値を簡単に区別する ことができましたが、先程の修正で 評価値 が -2 ~ 3 の 6 種類に増えた のでゲーム盤の 色で評価値を区別することが困難 になりました。そこで、ゲーム盤の上に評価値を直接表示する ことで、評価値が正しく計算されているかどうかを 確認できるようにする ことにします。

部分木のノードは Node クラスの draw_node メソッドで描画するので、下記のプログラムのように、その中でゲーム盤を表示する際に 上部に評価値を表示する ように修正します。

  • 8、9 行目:9 行目でゲーム盤の上部にノードの評価値を表示する。なお、評価値が計算されていないノード には score 属性が存在しない ので、組み込み関数 hasattr を使って score 属性が存在する場合のみ評価値を表示する ようにした。また、表示位置やフォントサイズは試行錯誤によって決めた値である
 1  import matplotlib.pyplot as plt
 2  from marubatsu import Marubatsu_GUI
 3  from tree import Node, Rect
 4
 5  def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
 6      Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
 7                               score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
 8      if hasattr(self, "score"):
 9          ax.text(dx, y - 0.1, self.score, fontsize=10)       
元と同じなので省略
10
11  Node.draw_node = draw_node
行番号のないプログラム
from tree import Node, Rect
import matplotlib.pyplot as plt
from marubatsu import Marubatsu_GUI

def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
    width = 8
    if ax is None:
        height = len(self.children) * 4
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")
        for childnode in self.children:
            childnode.height = 4
        self.height = height         
        
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
    bc = "red" if emphasize else None
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
                            score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
    if hasattr(self, "score"):
        ax.text(dx, y - 0.1, self.score, fontsize=10)
    rect = Rect(dx, y, 3, 3)
    # 子ノードが存在する場合に、エッジの線と子ノードを描画する
    if len(self.children) > 0:
        if maxdepth != self.depth:   
            ax.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="k", lw=lw)
            prevy = None
            for childnode in self.children:
                childnodey = dy + (childnode.height - 3) / 2
                if maxdepth is None:
                    Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True,
                                            score=getattr(childnode, "score", None), dx=dx+5, dy=childnodey, lw=lw)
                edgey = childnodey + 1.5
                ax.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="k", lw=lw)
                if prevy is not None:
                    ax.plot([dx + 4 , dx + 4], [prevy, edgey], c="k", lw=lw)
                prevy = edgey
                dy += childnode.height
        else:
            ax.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
            
    return rect

Node.draw_node = draw_node
修正箇所
from tree import Node, Rect
import matplotlib.pyplot as plt
from marubatsu import Marubatsu_GUI

def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
                            score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
+   if hasattr(self, "score"):
+       ax.text(dx, y - 0.1, self.score, fontsize=10)       
元と同じなので省略

Node.draw_node = draw_node

上記の修正後に、下記のプログラムを実行すると、実行結果のようにゲーム盤の上部に評価値が表示されるようになります。

Mbtree_GUI(bftree_shortest_victory)

実行結果

上部の評価値の表示の一部が表示されない問題の修正

上記の実行結果から、一番上の評価値の一部が表示されない という問題があることがわかります。これは、評価値の文字の上部が、部分木を表示する Figure の上端からはみ出てしまったことが原因 です。この問題を解決する方法を少し考えてみて下さい。

Mbtree_GUI クラスの __init__ メソッドの修正

この問題を解決する方法の一つに、Figure に表示する内容を すべて少しだけ下にずらす という方法がありますが、その方法は プログラムの修正がかなり大変です

別の方法として、Axes の y 座標の表示範囲を少しだけ上部に広げる という方法があります。試行錯誤した結果、表示範囲を 1 だけ上に広げる ことで 評価値の表示がはみでなくなる ことがわかったので、そのように表示範囲を広げることにします。

Axes の y 座標表示範囲を上に 1 つ広げる ということは、表示範囲の高さが 1 増える ということなので、下記のプログラムのように Mbtree_GUI クラスの __init__ メソッドを修正して Figure の高さを表す height 属性の値を 1 増やす 必要があります。

  • 5 行目height 属性の値を 64 から 65 に修正する
  • 7 行目__init__ メソッドを、クラスの定義の外で修正できるようにするために、super の記述を修正する。忘れた方は以前の記事を復習する事
1  def __init__(self, mbtree, size=0.15):
2      self.mbtree = mbtree
3      self.size = size
4      self.width = 50
5      self.height = 65
6      self.selectednode = self.mbtree.root
7      super(Mbtree_GUI, self).__init__()
8
9  Mbtree_GUI.__init__ = __init__ 
行番号のないプログラム
def __init__(self, mbtree, size=0.15):
    self.mbtree = mbtree
    self.size = size
    self.width = 50
    self.height = 65
    self.selectednode = self.mbtree.root
    super(Mbtree_GUI, self).__init__()

Mbtree_GUI.__init__ = __init__ 
修正箇所
def __init__(self, mbtree, size=0.15):
    self.mbtree = mbtree
    self.size = size
    self.width = 50
-   self.height = 64
+   self.height = 65
    self.selectednode = self.mbtree.root
-   super().__init__()
+   super(Mbtree_GUI, self).__init__()

Mbtree_GUI.__init__ = __init__ 

update_gui の修正

次に、Axes の表示範囲を設定する処理 を行う update_gui を下記のプログラムのように修正します。なお、同様の処理は以前の記事で深さが 0 のゲーム盤の枠線が Figure の外にはみ出て表示されるという問題を修正するために、3 行目で x 座標に対しても行っています。

  • 4 行目:Axes の y 座標の表示範囲の最小値と最大値を 1 減らして表示範囲を上にずらす
1  def update_gui(self):
2      self.ax.clear()
3      self.ax.set_xlim(-1, self.width - 1)
4      self.ax.set_ylim(-1, self.height - 1)   
元と同じなので省略
5    
6  Mbtree_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(-1, self.height - 1)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    centernode = self.selectednode
    while centernode.depth > 6:
        centernode = centernode.parent
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
                            show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
    
    disabled = self.selectednode.parent is None
    self.set_button_status(self.left_button, disabled=disabled)
    disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
    self.set_button_status(self.right_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
    self.set_button_status(self.up_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
    self.set_button_status(self.down_button, disabled=disabled)
    
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
-   self.ax.set_ylim(0, self.height)   
+   self.ax.set_ylim(-1, self.height - 1)   
元と同じなので省略
    
Mbtree_GUI.update_gui = update_gui

上記の修正後に下記のプログラムを実行すると、実行結果のように評価値がはみ出なくなりますが、灰色の背景の部分が上部に広がらないという問題が生じます。

Mbtree_GUI(bftree_shortest_victory)

実行結果(上部のみを表示しています)

Mbtree クラスの draw_subtree の修正

背景を灰色で表示する処理 は、Mbtree クラスの draw_subtree メソッド内で行っているので、その処理を下記のプログラムのように修正します。

  • 7、8 行目:灰色の長方形の表示位置の y 座標を -1 に修正し、表示する高さが増えたので高さを height + 1 に修正する1
 1 import matplotlib.patches as patches
 2
 3  def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None, isscore=False, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 4      if show_bestmove:
 5          bestx = 5 * maxdepth + 4
 6          bestwidth = 50 - bestx
 7          ax.add_artist(patches.Rectangle(xy=(bestx, -1), width=bestwidth,
 8                                          height=height + 1, fc="lightgray"))
元と同じなので省略
 9                
10  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None, isscore=False, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        """ノードを表示する暗さを計算して返す."""
        
        if show_bestmove:
            if node.parent is None:
                return 0
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    if show_bestmove:
        bestx = 5 * maxdepth + 4
        bestwidth = 50 - bestx
        ax.add_artist(patches.Rectangle(xy=(bestx, -1), width=bestwidth,
                                        height=height + 1, fc="lightgray"))
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                if show_bestmove and depth == maxdepth:
                    bestnode = node
                    while len(bestnode.bestmoves) > 0:
                        bestmove = bestnode.bestmoves[0]
                        bestnode = bestnode.children_by_move[bestmove]
                        dx = 5 * bestnode.depth
                        bestnode.height = 4
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
                        self.nodes_by_rect[rect] = bestnode                                          
                    
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
                
Mbtree.draw_subtree = draw_subtree
修正箇所
import matplotlib.patches as patches
 
def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None, isscore=False, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
    if show_bestmove:
        bestx = 5 * maxdepth + 4
        bestwidth = 50 - bestx
-       ax.add_artist(patches.Rectangle(xy=(bestx, -1), width=bestwidth,
+       ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
-                                       height=height, fc="lightgray"))
+                                       height=height + 1, fc="lightgray"))
元と同じなので省略
                
Mbtree.draw_subtree = draw_subtree

上記の修正後に下記のプログラムを実行すると、実行結果のように灰色の背景の部分が正しく表示されるようになります。

Mbtree_GUI(bftree_shortest_victory)

実行結果

評価値の確認

上記の修正後に、(1, 1) のみに着手が行われたノードをクリックすると下図のような表示が行われます。

図で水色で表示される局面は、7 手目で〇が勝利する局面 なので 評価値は 2 になるはず です。実際に上図では水色で表示される局面の上部には 2.0 が表示されるので 評価値が正しく計算されていることが確認できます。また、背景が灰色の部分の 水色の親ノードの評価値も 2.0 と表示される ので、他のノードの評価値の計算結果も正しい ことが確認できました。

このことから、一部の ノードの色が間違って表示される原因 は、評価値の計算以外 である 可能性が高い ことがわかります。

評価値に局面の背景色を表示する処理の確認

次に間違っている可能性が高い のは、評価値に合わせて背景色を表示する処理 なので、その処理を調べることにします。背景色を表示するのは下記の Marubatsu_GUI クラスの draw_board メソッドのプログラムの 4 ~ 11 行目の部分です。

 1  @staticmethod
def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2): 
 2      # 結果によってゲーム盤の背景色を変更する
 3      if show_result:
 4          if score is None and mb.status == Marubatsu.PLAYING:
 5              bgcolor = "white"
 6          elif score == 1 or mb.status == Marubatsu.CIRCLE:
 7              bgcolor = "lightcyan"
 8          elif score == -1 or mb.status == Marubatsu.CROSS:
 9              bgcolor = "lavenderblush"
10          else:
11              bgcolor = "lightyellow"
12          rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
13                                  height=mb.BOARD_SIZE, fc=bgcolor)
14          ax.add_patch(rect)

上記のプログラムから、先程の図で間違った色で表示されている、ゲーム中評価値が 2 の局面背景色がどのように表示されるか を以下のように検証します。

  • ゲーム中の場合は mb.status の値が Marubatsu.PLAYING である
  • 評価値が 2 の場合は score の値が 2 である
  • 従って、4、6、8 行目の条件式はいずれも False になるので、11 行目が実行され、背景色が黄色(lightyellow)になる

上記からゲーム中で評価値が 2 の局面が 間違った黄色で表示される ことが確認できました。

このようなことが起きる原因は、評価値の範囲 を -1, 0, 1 から -2 ~ 3 の範囲の整数に 変更したにも関わらず背景色の表示を行うプログラムの処理を変更していない ためです。どのようにプログラムを修正すれば良いかについて少し考えてみて下さい。

評価値が正の場合〇 の必勝の局面負の場合× の必勝の局面それ以外 の 0 の場合引き分けの局面 と判定するようにすることで、それぞれの状況の局面の背景色を正しく表示することができるようになります。下記はそのように draw_board メソッドを修正したプログラムです。

  • 7 行目:評価値が正の場合に背景色を水色にするように修正する
  • 9 行目:評価値が負の場合に背景色を赤色にするように修正する
 1  @staticmethod
 2  def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2): 
 3      # 結果によってゲーム盤の背景色を変更する
 4      if show_result:
 5          if score is None and mb.status == Marubatsu.PLAYING:
 6              bgcolor = "white"
 7          elif score > 0 or mb.status == Marubatsu.CIRCLE:
 8              bgcolor = "lightcyan"
 9          elif score < 0 or mb.status == Marubatsu.CROSS:
10              bgcolor = "lavenderblush"
11          else:
12              bgcolor = "lightyellow"
13          rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
14                                  height=mb.BOARD_SIZE, fc=bgcolor)
15          ax.add_patch(rect)
元と同じなので省略
16
17  Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
@staticmethod
def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2): 
    # 結果によってゲーム盤の背景色を変更する
    if show_result:
        if score is None and mb.status == Marubatsu.PLAYING:
            bgcolor = "white"
        elif score > 0 or mb.status == Marubatsu.CIRCLE:
            bgcolor = "lightcyan"
        elif score < 0 or mb.status == Marubatsu.CROSS:
            bgcolor = "lavenderblush"
        else:
            bgcolor = "lightyellow"
        rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                height=mb.BOARD_SIZE, fc=bgcolor)
        ax.add_patch(rect)

    # ゲーム盤の枠を描画する
    for i in range(1, mb.BOARD_SIZE):
        ax.plot([dx, dx + mb.BOARD_SIZE], [dy + i, dy + i], c="k", lw=lw) # 横方向の枠線
        ax.plot([dx + i, dx + i], [dy, dy + mb.BOARD_SIZE], c="k", lw=lw) # 縦方向の枠線

    # ゲーム盤のマークを描画する
    for y in range(mb.BOARD_SIZE):
        for x in range(mb.BOARD_SIZE):
            color = "red" if (x, y) == mb.last_move else "black"
            Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb.board[x][y], color, lw=lw)

    # darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
    if darkness > 0:
        ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                        height=mb.BOARD_SIZE, fc="black", alpha=darkness))

    # bc が None でない場合はその色で bw の太さで外枠を描画する
    if bc is not None:
        frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
        ax.add_patch(frame)
        
Marubatsu_GUI.draw_board = draw_board
修正箇所
@staticmethod
def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2): 
    # 結果によってゲーム盤の背景色を変更する
    if show_result:
        if score is None and mb.status == Marubatsu.PLAYING:
            bgcolor = "white"
-       elif score == 1 or mb.status == Marubatsu.CIRCLE:
+       elif score > 0 or mb.status == Marubatsu.CIRCLE:
            bgcolor = "lightcyan"
-       elif score == -1 or mb.status == Marubatsu.CROSS:
+       elif score < 0 or mb.status == Marubatsu.CROSS:
            bgcolor = "lavenderblush"
        else:
            bgcolor = "lightyellow"
        rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                height=mb.BOARD_SIZE, fc=bgcolor)
        ax.add_patch(rect)
元と同じなので省略
        
Marubatsu_GUI.draw_board = draw_board

上記の修正後に下記のプログラムを実行して (1, 1) のみに着手が行われている局面を選択すると、実行結果のように背景色が正しく表示されるようになったことを確認できます。

Mbtree_GUI(bftree_shortest_victory)

実行結果

また、(1, 1)、(1, 0)、(0, 0)、(2, 0) の順で着手を行った局面を選択すると、下図のように すぐに勝利できる (2, 2) を着手した局面の 評価値 が 3最も高くなりそれ以外の水色 の最善手を着手した局面の 評価値が 2 となるため、(2, 2) 以外 を着手したの 局面が暗く表示されるよう になることが確認できます。そのため、このゲーム木の評価値を使って着手を行う AI は、最善手の中で 最も早く勝利できる合法手を選択する ようになります。

データの保存

ゲーム木のデータが正しいことが確認できたので、下記のプログラムで作成したゲーム木のデータをファイルに保存することにします。

bftree_shortest_victory.save("../data/bftree_shortest_victory")
dftree_shortest_victory.save("../data/dftree_shortest_victory")

実行結果

save completed.
save completed.

必敗の局面での最善手の優劣

以前の記事で、必敗の局面でも 人間から見て最善を尽くす ように見える着手を行うという観点では、最善手に優劣が存在する ことを説明しました。

例えば、下記の (1, 1)、(1, 0)、(0, 0) の順で着手を行った局面は × の必敗の局面 なので、相手が最善手を着手し続けた場合は どこに着手を行っても最終的には × が敗北します

しかし、人間の立場でこの局面を見る と、次の 〇の手番の勝利を阻止しない (2, 2) 以外に着手を行うのは、勝負を途中であきらめる 投げやりな着手のように見えてしまいます

また、相手が最強の AI でない場合は、相手がミスをする可能性がある ため、すぐに負けないように粘ることで、勝ちを拾える可能性 があります。そのため、必敗の局面 ではなるべく 敗北を先に延ばすような着手を行ったほうが良い と考えることもできます。

実は 必敗の局面 でなるべく 敗北を先に延ばすような着手 は、bftree_shortest_victory の評価値を使って計算することが可能 です。その理由は、敗北を先に延ばすということは、相手の勝利を遅らせるということと同じ であり、先程の評価値の計算方法では、相手が速く勝利するほど自分にとって評価値が不利になる ように計算が行われるからです。

実際に、上図では次の相手の手番での 敗北を阻止する (2, 2) を着手した局面の評価値が 2それ以外の着手を行った局面の評価値は 3 となっており、× の手番の局面では評価値が小さいほうが有利 なので、この評価値に従って着手を行う AI は (2, 2) のみを選択 します。

bftree_shortest_victory を利用した AI

bftree_shortest_victory を利用した AI は、ゲーム木のデータを利用して着手を選択する ai_gt42を、キーワード引数に mbtree=bftree_shortest_victory を記述して呼び出すことで作成することができます。下記は、play メソッドを使って bftree_shortest_victory を利用した AI と人間が対戦するプログラムです。

from ai import ai_gt4

mb = Marubatsu()
mb.play(ai=[ai_gt4, None], params=[{"mbtree": bftree_shortest_victory}, {}], gui=True)

上記を実行し、リセットボタンと待ったボタンを利用して、下図のように (0, 0)、(1, 0)、(0, 0) の順で着手が行われた局面を作ってください。この局面で (2, 0) に着手を行う と、ai_gt4 は必ず (2, 2) を着手して勝利 します。実際に確認して下さい。

なお、下部に表示される GUI の部分木 は、今回の記事で作成した bftree_shortest_victory ではなく、aidata.mbtree に保存されているゲーム木のデータを使って表示しているので、評価値には -1, 0, 1 のみが計算されます。GUI の部分木を bftree_shortest_victory を使って表示できるようにする改良は、次回の記事で行うことにします。

また、上図をよく見ると 評価値の文字上のゲーム盤少しに重なって表示されてしまう という問題があることがわかります。このような表示が行われる原因と修正方法についても次回の記事で説明することにします。

ai_gt6 を利用した AI

bftree_shortest_victory はデータサイズが大きいので、以前の記事で行ったように、データサイズの小さい board 属性を利用した局面と最善手の対応表を作成 し、ai_gt6 を使って bftree_shortest_victory で計算した評価値を使った AI を実行したほうが良いでしょう。

下記は、以前の記事で記述した、ゲーム木のデータから bestmoves_by_board を計算するプログラム です。また、計算した bestmoves_by_board を今後も利用できるようにするためには、このデータを ファイルに保存する必要があります

bestmoves_by_board = {}
for node in tqdm(mbtree.nodelist):
    txt = node.mb.board_to_str()
    if not txt in bestmoves_by_board.keys():
        bestmoves_by_board[txt] = node.bestmoves

ゲーム木から bestmoves_by_board を計算してファイルに保存する処理は、今後の記事で別の方法で評価値を計算するゲーム木を作成した際にも行う予定なので、下記の関数を定義することにします。

名前calc_and_save_bestmoves_by_board とする
処理:ゲーム木から board 属性を利用した局面と最善手の対応表のデータを計算してファイルに保存し、返り値として返す
入力:ゲーム木のデータを仮引数 mbtree に、計算したデータを保存するファイルのパスを path に代入する
出力:計算した対応表のデータ

下記は、calc_and_save_bestmoves_by_board の定義です。また、この関数は util.py に保存することにします。

from tqdm import tqdm
import pickle
import gzip

def calc_and_save_bestmoves_by_board(mbtree, path):
    bestmoves_by_board = {}
    for node in tqdm(mbtree.nodelist):
        txt = node.mb.board_to_str()
        if not txt in bestmoves_by_board.keys():
            bestmoves_by_board[txt] = node.bestmoves

    with gzip.open(path, "wb") as f:
        pickle.dump(bestmoves_by_board, f)
    
    return bestmoves_by_board

次に、下記のプログラムで bftree_shortest_victory から bestmoves_by_board を計算し、bestmoves_by_board_shortest_victory.dat というファイルに保存します。

bestmoves_by_board = calc_and_save_bestmoves_by_board(bftree_shortest_victory,
                            "../data/bestmoves_by_board_shortest_victory.dat")

実行結果

100%|██████████| 549946/549946 [00:00<00:00, 562598.73it/s]

実行結果は省略しますが、人間とこの AI との対戦は下記のプログラムで行えます。

from ai import ai_gt6

mb.play(ai=[None, ai_gt6], params=[{}, {"bestmoves_by_board": bestmoves_by_board}], gui=True)

先ほどと同様に (0, 0)、(1, 0)、(0, 0) の順で着手が行われた局面で AI が (2, 2) のみを着手することを確認して下さい。

gui_play への AI の登録

次に、gui_play でこの AI との対戦を行えるようにします が、その際に既に ai_gt6 を利用した別の AI が Dropdown に登録されている ので、Dropdown に登録する AI の名前を考える必要があります。そこでこの AI の Dropdown の項目名を shortest victory の頭文字を取って ai_gtsv とすることにします。

下記は gui_play を修正したプログラムです。

  • 7 行目:ファイルから今回の記事で作成した AI に必要なパラメータのデータを読み込む
  • 8 行目ai_dictai_gtsv のキーの値に今回の記事で作成した AI のデータを登録する
1  import ai as ai_module
2  from util import load_bestmoves
3
4  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
5          bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
6          ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
7          bestmoves_by_board_sv = load_bestmoves("../data/bestmoves_by_board_shortest_victory.dat")
8          ai_dict["ai_gtsv"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board_sv})
元と同じなので省略
行番号のないプログラム
import ai as ai_module
from util import load_bestmoves

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    if params is None:
        params = [{}, {}]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": ( None, {} ) }
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = (getattr(ai_module, ai_name), {})
        bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
        ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
        bestmoves_by_board_sv = load_bestmoves("../data/bestmoves_by_board_shortest_victory.dat")
        ai_dict["ai_gtsv"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board_sv})
    
    mb = Marubatsu()
    mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
修正箇所
import ai as ai_module
from util import load_bestmoves

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
        bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
        ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
        bestmoves_by_board_sv = load_bestmoves("../data/bestmoves_by_board_shortest_victory.dat")
        ai_dict["ai_gtsv"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board_sv})
元と同じなので省略

下記のプログラムで gui_play() を実行した後で、下記の手順で操作を行うと、ai_gtsv が必ず (2, 2) に着手を行って勝利する ことを確認して下さい。

gui_play()
  1. gui_play() を実行する
  2. (1, 1)、(1, 0)、(0, 0) の順で着手を行う
  3. 〇 の担当を ai_gtsv に変更し、「変更」ボタンをクリックする(下図)
  4. (2, 0) に着手を行う

また、下記の手順によって、× の必敗の局面で ai_gt6開いているすべてのマスに着手を行う場合がある のに対し、ai_gtsv必ず (2, 2) に着手を行って敗北を引き延ばす着手を行う ことを確認して下さい。

  1. 手番の担当を両方とも人間にし、リセットボタンをクリックする
  2. (1, 1)、(1, 0) の順で着手を行う
  3. × の担当を ai_gt6 に変更し、「変更」ボタンをクリックする
  4. (0, 0) に着手を行い、AI がどこに着手を行うかを確認する
  5. 待ったボタンをクリックして (0, 0) に着手を行う操作を何度か繰り返す
  6. 待ったボタンをクリックし、× の担当を ai_gtsv に変更し、「変更」ボタンをクリックする
  7. (0, 0) に着手を行い、AI がどこに着手を行うかを確認する
  8. 待ったボタンをクリックして (0, 0) に着手を行う操作を何度か繰り返す

今回の記事のまとめ

今回の記事では、ゲーム木を利用した AI が、最善手の優劣を考慮して着手を行うようにする方法を紹介しました。

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

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu_new.py 今回の記事で更新した marubatsu.py
tree_new.py 今回の記事で更新した tree.py
util_new.py 今回の記事で更新した util.py
bftree_shortest_victory.mbtree 今回の記事で作成した bftree_shortest_victory.mbtree
dftree_shortest_victory.mbtree 今回の記事で作成した dftree_shortest_victory.mbtree
bestmoves_by_board_shortest_victory.dat 今回の記事で作成した bestmoves_by_board_shortest_victory.dat

次回の記事

  1. この height は、Mbtree_GUI クラスの height 属性ではなく、draw_subtree メソッドで計算した中心となるノードの表示の高さ(centernode.height)のことなので、ゲーム盤の上部に表示する評価値を表示する部分は含まれません

  2. ゲーム木を表すデータをキーワード引数 mbtree に記述して呼び出す ai_gt1ai_gt2ai_gt3 を利用しても構いません

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?