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を一から作成する その126 AI が計算した候補手または評価値のゲーム盤への表示

Last updated at Posted at 2024-10-20

目次と前回の記事

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

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

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

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

Marubatsu_GUI クラスの改良の続き

以前の記事で、Marubatsu_GUI クラスの改良を開始しました。今回の記事ではその中の最後の、下記の改良の実装を開始します。

  • ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できるようにする

AI が計算する評価値の表示のメリット

現状では、下記のプログラムのように、ai14s などの一部の AI の関数はキーワード引数 debug=True を記述して AI の関数を呼び出すことで、それぞれの 合法手を着手した場合に計算される評価値を表示する ようになっています。

from marubatsu import Marubatsu
from ai import ai14s

mb = Marubatsu()
ai14s(mb, debug=True)

実行結果

Start ai_by_score
Turn o
...
...
...

legal_moves [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (0, 0)
Turn x
O..
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 5,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score 1.5 best score -inf
UPDATE
  best score 1.5
  best moves [(0, 0)]
====================
move (1, 0)
Turn x
.O.
...
...
略

上記のようなデバッグの表示は、評価値を計算する際の 詳細なデータが表示されるという点では便利 ですが、一覧性に欠ける という欠点があります。そこで、ゲーム盤のそれぞれのマスに、そのマスに着手を行った局面に対して AI が計算する評価値を表示するという工夫を行うことにします。

また、ai1 のように、評価値を計算しない AI の場合 はゲーム盤のマスに、その AI が計算した 候補手であることがわかるような、何らかの表示を行う ことにします。

評価値を表示するために必要な処理の実装

現状の AI の関数 は、以下の返り値を返します。

  • 通常は、合法手の中から AI が選択した 1 つの着手を返す
  • キーワード引数 candidate=True を記述して呼び出した場合は、その AI が最善手とみなした 候補手の一覧を list で返す。ただし、現状では、一部の AI の関数のみこの機能が実装されている

合法手を着手した局面の評価値を表示するためには、上記以外に、それぞれの 合法手を着手した局面の評価値の情報 を AI の関数が 返り値として返す必要 があります。

そこで、AI の関数を以下のように修正することにします。

  • 通常は、合法手の中から AI 選択した 1 つの着手を返す点は変わらない
  • デフォルト値を Falst とする仮引数 analyze を追加し、True が代入された場合 は、「その AI が最善手とみなした 候補手の一覧」と「それぞれの 合法手を着手した局面の評価値」を表すデータを下記の dict で返すようにする。このデータは、AI の処理を分析(analyze)するために利用できるので、キーワード引数の名前を analyze とした
    • candidate というキーの値に、その AI が最善手とみなした 候補手の一覧を表す list を代入する
    • score_by_move というキーの値に、それぞれの 合法手を着手した局面の評価値を表す dict を代入する。ただし、ai1 のように、評価値を計算しない AI の関数の場合は、None を代入 する
  • 仮引数 candidate の機能は、仮引数 analyze の機能に含まれるので 削除する

ai3s の修正

まず、評価値を計算する関数から修正 することにします。そのような関数は ai1s から ai14s まで 14 種類もありますが、真ん中のマスに優先的に着手を行うという単純な ルール3 を実装した ai3s の修正を最初に行う ことにします。

具体的には ai3s は下記のプログラムのような単純な方法で評価値の計算を行います。

  • 中央の (1, 1) に着手が行われていた場合は評価値として 1 を計算する
  • それ以外のマスに着手を行がわれていた場合は評価値として 0 を計算する
def ai3s(mb, debug=False, candidate=False):
    def eval_func(mb):
        if mb.last_move == (1, 1):
            return 1
        else:
            return 0
        
    return ai_by_score(mb, eval_func, debug=debug, candidate=candidate)

左上から順に空いているマスを探し、最初に見つかったマスに着手するという、ルール 1 を実装した ai1s を選択しなかった のは、キーワード引数 rand=False を記述して ai_by_score を呼び出すことで先頭の合法手を選択するという、評価値をそのまま利用しない特殊な処理を行っているから です。そのため、ai1s の修正は、他の ai2s ~ ai14s とは若干異なる方法で行う必要があり、その修正は次回の記事で行います。

ランダムな着手を行うという、ルール 2 を実装した ai2s を選択しなかった のは、常に全ての合法手が最善手となり、全ての空いているマスに 0 という評価値が表示されるため、見た目があまり面白くない からです。

ai_gt1 ~ ai_gt6 も評価値を元に候補手を計算しますが、その場で評価値を計算していない 点が ai3s と異なるため、修正方法は ai3s と大きく異なります。それらの修正は次回の記事で行います。

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

  • 1 行目:仮引数 candidateanalyze に修正する
  • 7 行目:キーワード引数 candidate=candidateanalyze=analyze に修正する
1  def ai3s(mb, debug=False, analyze=False):
2      if mb.last_move == (1, 1):
3          return 1
4      else:
5          return 0
6        
7      return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)
行番号のないプログラム
def ai3s(mb, debug=False, analyze=False):
    def eval_func(mb):
        if mb.last_move == (1, 1):
            return 1
        else:
            return 0
        
    return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)
修正箇所
-def ai3s(mb, debug=False, candidate=False):
+def ai3s(mb, debug=False, analyze=False):
    def eval_func(mb):
        if mb.last_move == (1, 1):
            return 1
        else:
            return 0        
-   return ai_by_score(mb, eval_func, debug=debug, candidate=candidate)
+   return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)

ai_by_score の修正

次に、ai3s の 7 行目で呼び出す ai_by_score を以下のプログラムのように修正します。

  • 5 行目:仮引数 candidateanalyze に修正する
  • 7、8 行目analyzeTrue の場合に、「それぞれの合法手を着手した局面の評価値」を記録する score_by_move を空の dict で初期化する
  • 12、13 行目analyzeTrue の場合に、move を着手した局面の評価値を score_by_move に記録する
  • 14 ~ 18 行目analyzeTrue の場合に先程説明した dict を返り値として返すように修正する
 1  from copy import deepcopy
 2  from random import choice
 3  from ai import dprint
 4
 5  def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):   
元と同じなので省略
 6      best_moves = []
 7      if analyze:
 8          score_by_move = {}
 9      for move in legal_moves:
元と同じなので省略
10          score = eval_func(mb)
11          dprint(debug, "score", score, "best score", best_score)
12          if analyze:
13              score_by_move[move] = score
元と同じなので省略
14      if analyze:
15          return {
16              "candidate": best_moves,
17              "score_by_move": score_by_move,
18          }
19      elif rand:   
20          return choice(best_moves)
21      else:
22          return best_moves[0]
行番号のないプログラム
from copy import deepcopy
from random import choice
from ai import dprint

def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):   
    dprint(debug, "Start ai_by_score")
    dprint(debug, mb_orig)
    legal_moves = mb_orig.calc_legal_moves()
    dprint(debug, "legal_moves", legal_moves)
    best_score = float("-inf")
    best_moves = []
    if analyze:
        score_by_move = {}
    for move in legal_moves:
        dprint(debug, "=" * 20)
        dprint(debug, "move", move)
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        dprint(debug, mb)
        
        score = eval_func(mb)
        dprint(debug, "score", score, "best score", best_score)
        if analyze:
            score_by_move[move] = score
        
        if best_score < score:
            best_score = score
            best_moves = [move]
            dprint(debug, "UPDATE")
            dprint(debug, "  best score", best_score)
            dprint(debug, "  best moves", best_moves)
        elif best_score == score:
            best_moves.append(move)
            dprint(debug, "APPEND")
            dprint(debug, "  best moves", best_moves)

    dprint(debug, "=" * 20)
    dprint(debug, "Finished")
    dprint(debug, "best score", best_score)
    dprint(debug, "best moves", best_moves)
    if analyze:
        return {
            "candidate": best_moves,
            "score_by_move": score_by_move,
        }
    elif rand:   
        return choice(best_moves)
    else:
        return best_moves[0]
修正箇所
from copy import deepcopy
from random import choice
from ai import dprint

-def ai_by_score(mb_orig, eval_func, debug=False, rand=True, candidate=False):   
+def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):   
元と同じなので省略
    best_moves = []
+   if analyze:
+       score_by_move = {}
    for move in legal_moves:
元と同じなので省略
        score = eval_func(mb)
        dprint(debug, "score", score, "best score", best_score)
+       if analyze:
+           score_by_move[move] = score
元と同じなので省略
-   if candidate:
-       return best_moves
+   if analyze:
+       return {
+           "candidate": best_moves,
+           "score_by_move": score_by_move,
+       }
    elif rand:   
        return choice(best_moves)
    else:
        return best_moves[0]

上記の修正後に、下記のプログラムでキーワード引数 analyze=True を記述してゲーム開始時の局面に対して ai3s を呼び出すと、実行結果のように下記のような候補手の一覧と、それぞれの合法手を着手した局面の評価値を記録した dict が返されることが確認できます。なお、わかりやすさを重視して、結果を pprint で表示しました。

  • 候補手の一覧は (1, 1) のみを要素とする list
  • 局面の評価値は (1, 1) のキーの値が 1、それ以外のキーの値が 0 となる dict
from pprint import pprint
 
pprint(ai3s(mb, analyze=True))

実行結果

{'candidate': [(1, 1)],
 'score_by_move': {(0, 0): 0,
                   (0, 1): 0,
                   (0, 2): 0,
                   (1, 0): 0,
                   (1, 1): 1,
                   (1, 2): 0,
                   (2, 0): 0,
                   (2, 1): 0,
                   (2, 2): 0}}

また、下記のプログラムのように (1, 1) に着手を行った後で ai3s を実行すると、実行結果のように全ての合法手が候補手に、全ての着手に対する局面の評価値が 0 になることが確認できます。

mb.move(1, 1)
pprint(ai3s(mb, analyze=True))

実行結果

{'candidate': [(0, 0), (1, 0), (2, 0), (0, 1), (2, 1), (0, 2), (1, 2), (2, 2)],
 'score_by_move': {(0, 0): 0,
                   (0, 1): 0,
                   (0, 2): 0,
                   (1, 0): 0,
                   (1, 2): 0,
                   (2, 0): 0,
                   (2, 1): 0,
                   (2, 2): 0}}

ai3 の修正

次に、評価値を計算しない AI として、ai3s と同じルール 3で候補手を計算する下記のプログラムで定義される ai3 を修正することにします。

def ai3(mb):
    if mb.board[1][1] == Marubatsu.EMPTY:
        return 1, 1
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)

プログラムの改良

評価値を計算しない AI では、何らかの手順で候補手の一覧を計算し、その中からランダムに 1 つを選択するという処理を行う必要があります。

ai3 では、(1, 1) に着手が行われていない場合は、(1, 1) を返し、そうでなければ合法手の一覧からランダムに 1 を選択して返すという処理を行っていますが、この 2 つの処理を下記の方法で 1 つにまとめることができます。

  • (1, 1) のマスが開いている場合は、候補手の一覧として (1, 1) のみを要素として持つ [(1, 1)] という list を計算する
  • (1, 1) のマスが開いていない場合は合法手の一覧を候補手の一覧とする
  • 候補手の一覧の中からランダムに 1 つを選択して返す

上記の手順で正しい処理が行われる理由は、[(1, 1)] のような、要素が 1 つしかない list に対して choice でランダムに要素を選択した場合は、必ず (1, 1) が選択されるからです。下記は、そのように ai3 を修正したプログラムです。

  • 2、3 行目:(1, 1) のマスが開いている場合に候補手を表す candidate(1, 1) のみを要素とする list を代入するように修正する
  • 4、5 行目:(1, 1) のマスが開いていない場合は candidate に合法手の一覧を代入するように修正する。
  • 6 行目choice を利用して候補手の中からランダムに一つを選択した着手を返す
1  def ai3(mb):
2      if mb.board[1][1] == Marubatsu.EMPTY:
3          candidate = [(1, 1)]
4      else:
5          candidate = mb.calc_legal_moves()
6      return choice(candidate)
修正箇所
def ai3(mb):
    if mb.board[1][1] == Marubatsu.EMPTY:
-       return (1, 1)]
+       candidate = [(1, 1)]
-   legal_moves = mb.calc_legal_moves()
-   return choice(legal_moves)
+   else:
+       candidate = mb.calc_legal_moves()
+   return choice(candidate)

3 行目を candidate = (1, 1) のように記述して ai3 を呼び出してもエラーは発生しませんが、(1, 1) のマスが開いていた場合は 返り値に 1 が返る という バグが発生する 点に注意して下さい。

その理由は、choice が list や tuple などの 反復可能オブジェクトの中からランダムに 1 つの要素を選択する という処理を行うからです。candidate(1, 1) が代入されていた場合は、choice((1, 1)) が実行され、(1, 1) という tuple の中からランダムに 1 つの要素が選択されます。(1, 1) の要素はどちらも 1 なので、必ず 1 が返される ことになります。

仮引数 analyze の追加

次に、下記のプログラムのように ai3 に仮引数 analyze を追加し、analyzeTrue の場合の処理を記述します。上記で ai3 を修正した理由 は、下記のプログラムの 6 ~ 12 行目のように、返り値を計算する処理 を (1, 1) のマスが開いているかどうかに関わらず、一つにまとめることができるようにするため です

  • 1 行目:仮引数 analyze を追加する
  • 6 ~ 10 行目analyzeTrue の場合に返り値として返す dict を計算する。ai3 は評価値を計算しないので、score_by_move のキーの値は None とする
  • 11、12 行目analyzeFalse の場合は元と同じ処理を行う
 1  def ai3(mb, analyze=False):
 2      if mb.board[1][1] == Marubatsu.EMPTY:
 3          candidate = [(1, 1)]
 4      else:
 5          candidate = mb.calc_legal_moves()
 6      if analyze:
 7          return {
 8              "candidate": candidate,
 9              "score_by_move": None
10          }
11      else:
12          return choice(candidate)
行番号のないプログラム
def ai3(mb, analyze=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        candidate = [(1, 1)]
    else:
        candidate = mb.calc_legal_moves()
    if analyze:
        return {
            "candidate": candidate,
            "score_by_move": None
        }
    else:
        return choice(candidate)
修正箇所
-def ai3(mb):
+def ai3(mb, analyze=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        candidate = [(1, 1)]
    else:
        candidate = mb.calc_legal_moves()
-   return choice(candidate)
+   if analyze:
+       return {
+           "candidate": candidate,
+           "score_by_move": None
+       }
+   else:
+       return choice(candidate)

上記の修正後に、下記のプログラムでキーワード引数 analyze=True を記述して ai3 を実行すると、実行結果のように正しいデータが返されることが確認できます。

mb.restart()
pprint(ai3(mb, analyze=True))

実行結果

{'candidate': [(1, 1)], 'score_by_move': None}

また、下記のプログラムのように (1, 1) に着手を行った後で ai3 を実行すると、実行結果のように全ての合法手が候補手になることが確認できます。

mb.move(1, 1)
pprint(ai3(mb, analyze=True))

実行結果

{'candidate': [(0, 0), (1, 0), (2, 0), (0, 1), (2, 1), (0, 2), (1, 2), (2, 2)],
 'score_by_move': None}

残りの修正

この後で行う必要がある修正は以下の通りです。

  • ai_by_score の仮引数 candidate を廃止したので、キーワード引数 candidate=candidate を記述して AI の関数を呼び出す処理を修正 する
  • 残りの AI の関数を修正する

キーワード引数 candidate=candidate を記述して AI の関数を呼び出す処理の修正

1 つ目の、キーワード引数 candidate=candidate を記述して AI の関数を呼び出す処理は、util.py で定義されている Check_solved クラスの is_strongly_solvedis_weakly_solved_r メソッドで記述されているので、それぞれを下記のプログラムのように修正します。

  • 7 行目:AI の関数呼び出しのキーワード引数 candidate=Trueanalyze=True に修正する。また、返り値が dict になった ので、candidate にはその dict の 候補手の一覧を表す candidate のキーの値を代入 するように修正する
 1  from util import Check_solved, load_bestmoves, load_mblist
 2  from tqdm import tqdm
 3
 4  @staticmethod
 5  def is_strongly_solved(ai, params=None, consider_samedata=True):
元と同じなので省略
 6      for mb in tqdm(mblist):
 7          candidate = set(ai(mb, analyze=True, **params)["candidate"])
 8          bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
 9          if candidate <= bestmoves:
10              count += 1
11          else:
12              incorrectlist.append((mb, candidate, bestmoves))
元と同じなので省略
13
14  Check_solved.is_strongly_solved = is_strongly_solved
行番号のないプログラム
from util import Check_solved, load_bestmoves, load_mblist
from tqdm import tqdm

@staticmethod
def is_strongly_solved(ai, params=None, consider_samedata=True):
    if Check_solved.bestmoves_by_board is None:
        Check_solved.bestmoves_by_board = load_bestmoves ("../data/bestmoves_by_board.dat")
    if consider_samedata:
        if Check_solved.mblist_by_board_min is None:
            Check_solved.mblist_by_board_min = load_mblist("../data/mblist_by_board_min.dat")
        mblist = Check_solved.mblist_by_board_min
    else:
        if Check_solved.mblist_by_board2 is None:
            Check_solved.mblist_by_board2 = load_mblist("../data/mblist_by_board2.dat") 
        mblist = Check_solved.mblist_by_board2        
    if params is None:
        params = {}
    count = 0
    incorrectlist = []
    for mb in tqdm(mblist):
        candidate = set(ai(mb, analyze=True, **params)["candidate"])
        bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
        if candidate <= bestmoves:
            count += 1
        else:
            incorrectlist.append((mb, candidate, bestmoves))
    nodenum = len(mblist)
    print(f"{count}/{nodenum} {count/nodenum*100:.2f}%")
    return count == nodenum, incorrectlist

Check_solved.is_strongly_solved = is_strongly_solved
修正箇所
from util import Check_solved, load_bestmoves, load_mblist
from tqdm import tqdm

@staticmethod
def is_strongly_solved(ai, params=None, consider_samedata=True):
元と同じなので省略
    for mb in tqdm(mblist):
-       candidate = set(ai(mb, candidate=True, **params))
+       candidate = set(ai(mb, analyze=True, **params)["candidate"])
        bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
        if candidate <= bestmoves:
            count += 1
        else:
            incorrectlist.append((mb, candidate, bestmoves))
元と同じなので省略

Check_solved.is_strongly_solved = is_strongly_solved
  • 4 行目is_strongly_solved の 7 行目の修正と同じ
1  @staticmethod
2  def is_weakly_solved_r(node, ai, turn, params, registered_boards):
元と同じなので省略
3      if turn == node.mb.turn:
4          moves = ai(node.mb, analyze=True, **params)["candidate"]
5      else:
6          moves = node.mb.calc_legal_moves()
元と同じなので省略
7
8  Check_solved.is_weakly_solved_r = is_weakly_solved_r
行番号のないプログラム
@staticmethod
def is_weakly_solved_r(node, ai, turn, params, registered_boards):
    txt = node.mb.board_to_str()
    if txt in registered_boards:
        return True
    Check_solved.count += 1
    registered_boards.add(txt)
    if node.mb.status == turn or node.mb.status == Marubatsu.DRAW:
        return True
    elif node.mb.status != Marubatsu.PLAYING:
        return False
        
    if turn == node.mb.turn:
        moves = ai(node.mb, analyze=True, **params)["candidate"]
    else:
        moves = node.mb.calc_legal_moves()
    for move in moves:
        childnode = node.children_by_move[move]
        if not Check_solved.is_weakly_solved_r(childnode, ai, turn, params, registered_boards):
            return False
    return True 

Check_solved.is_weakly_solved_r = is_weakly_solved_r
修正箇所
@staticmethod
def is_weakly_solved_r(node, ai, turn, params, registered_boards):
元と同じなので省略
    if turn == node.mb.turn:
-       moves = ai(node.mb, candidate=True, **params)
+       moves = ai(node.mb, analyze=True, **params)["candidate"]
    else:
        moves = node.mb.calc_legal_moves()
元と同じなので省略

Check_solved.is_weakly_solved_r = is_weakly_solved_r

上記の修正後に、下記のプログラムを実行して is_strongly_solvedis_weakly_solved を使って ai3s を判定 すると、強解決でも、弱解決でもないという、正しい判定が行われることが確認できます。また、本記事での記述は省略しますが、ai3 を判定しても同様の結果が得られるので、興味がある方は確認して下さい。

Check_solved.is_strongly_solved(ai3s)

実行結果

100%|██████████| 431/431 [00:00<00:00, 3315.25it/s]
117/431 27.15%

(False,
 [(<marubatsu.Marubatsu at 0x286603756d0>, {(1, 2), (2, 1), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x286603759d0>, {(1, 2), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660375cd0>, {(2, 1), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660375fd0>, {(0, 2), (1, 2), (2, 1)}, {(0, 2)}),
  (<marubatsu.Marubatsu at 0x286603762d0>, {(0, 2), (1, 2)}, {(0, 2)}),
  (<marubatsu.Marubatsu at 0x286603765d0>, {(0, 2), (2, 1)}, {(0, 2)}),
  (<marubatsu.Marubatsu at 0x286603768d0>, {(1, 1)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660376bd0>, {(0, 2), (1, 2), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660376e90>, {(1, 2), (2, 2)}, {(1, 2)}),
  (<marubatsu.Marubatsu at 0x28660377190>, {(0, 2), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660377490>, {(1, 1)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660377790>, {(1, 1)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x286603792d0>, {(0, 2), (2, 1), (2, 2)}, {(2, 1)}),
  (<marubatsu.Marubatsu at 0x28660379590>, {(0, 2), (2, 1)}, {(2, 1)}),
  (<marubatsu.Marubatsu at 0x28660379b90>, {(1, 1)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660379e90>, {(0, 2), (1, 2), (2, 1)}, {(2, 1)}),
  (<marubatsu.Marubatsu at 0x2866037a4d0>,
   {(0, 1), (0, 2), (1, 2), (2, 1), (2, 2)},
   {(1, 2)}),
略
Check_solved.is_weakly_solved(ai3s)

実行結果

   o False
   x False
Both False
False

残りの AI の関数の修正について

AI の関数はこれまでに 20 種類以上作成した ので、上記の修正を行うのは大変です。そこで、残りの AI の関数の修正は次回の記事で行うことにして、ゲーム盤のマスに そのマスに着手を行った局面に対する ai3s の評価値を表示する処理 を実装することにします。

なお、gui_play で〇×ゲームを GUI で遊ぶ際に、キーワード引数 candidate を記述して AI の関数を呼び出す処理を行っていない ので、下記のプログラムで gui_play を実行しても問題なく動作します。なお、実行結果はこれまでと同様なので省略します。

from util import gui_play

gui_play()

AI が計算した評価値または候補手の表示の実装

合法手を着手した局面の AI の評価値の表示を常に表示すると真剣勝負の邪魔になるので、前回の記事で作成した「状況」ボタンによってその表示を切り替えることにします。

また、具体的な表示は以下のようにすることにします。

  • 評価値を計算する AI の場合は マスの中 に AI が計算した 評価値を表示 する。また、そのマスが AI の 候補手に含まれている場合は赤字 で、含まれていない場合は黒字 で表示する
  • 評価値を計算しない AI の場合 はマスの中に、そのマスが AI の候補手に含まれている場合は「候補手」と表示 し、そうでない場合は何も表示しない
  • 上記の表示は前回の記事で表示した「そのマスに着手を行った場合の局面の状況を表す文字」の下に表示する

また、上記の処理は、前回の記事で Marubatsu_GUI クラスの update_gui の中に記述した、「そのマスに着手を行った場合の局面の状況を表す文字を表示する処理」の後に記述することにします。

下記は、そのように update_gui を修正したプログラムです。

  • 5 行目:marubatsu.py を修正した際に循環インポートが発生しないようにここで ai3s をローカルにインポートしている
  • 7 ~ 9 行目:キーワード引数 analyze=True を記述して現在の局面に対して ai3s を呼び出し、その返り値から「それぞれの合法手を着手した場合に計算される評価値」と「候補手の一覧」を表すデータを取り出して score_by_movecandidate に代入する
  • 18 ~ 20 行目score_by_moveNone ではない場合は、19 行目で move が候補手に含まれているかどうかで文字の色を計算し、20 行目で move に着手した場合の評価値を表示する。評価値を表示する y 座標の値は試行錯誤して決めたものである
  • 21、22 行目score_by_moveNone であり、なおかつ move が候補手に含まれている場合は、「候補手」と表示する。なお、文字の大きさを 5*self.size で計算して表示すると「候補手」の文字の右端がゲーム盤の枠に重なってしまうので小さくした
 1  from marubatsu import Marubatsu_GUI
 2
 3  def update_gui(self):
元と同じなので省略
 4      if self.show_status:
 5          from ai import ai3s
 6          bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
 7          analyze = ai3s(self.mb, analyze=True)
 8          score_by_move = analyze["score_by_move"]
 9          candidate = analyze["candidate"]
10          for move in self.mb.calc_legal_moves():
11              x, y = move
12              mb = deepcopy(self.mb)
13              mb.move(x, y)
14              score = self.score_table[mb.board_to_str()]["score"]
15              color = "red" if move in bestmoves else "black"
16              text = calc_status_txt(score)
17              ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
18              if score_by_move is not None:
19                  color = "red" if move in candidate else "black"
20                  ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
21              elif move in candidate:
22                  ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
23        
24  Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
from marubatsu import Marubatsu_GUI

def update_gui(self):
    def calc_status_txt(score):
        if score > 0:
            return ""
        elif score == 0:
            return ""
        else:
            return "×"
    
    ax = self.ax

    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()

    # y 軸を反転させる
    ax.invert_yaxis()

    # 枠と目盛りを表示しないようにする
    ax.axis("off")   

    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
            text += " " + calc_status_txt(score)
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=7*self.size)

    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        analyze = ai3s(self.mb, analyze=True)
        score_by_move = analyze["score_by_move"]
        candidate = analyze["candidate"]
        for move in self.mb.calc_legal_moves():
            x, y = move
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if move in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
            if score_by_move is not None:
                color = "red" if move in candidate else "black"
                ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
            elif move in candidate:
                ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
                
    self.update_widgets_status()

    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
修正箇所
from marubatsu import Marubatsu_GUI

def update_gui(self):
元と同じなので省略
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
+       analyze = ai3s(self.mb, analyze=True)
+       score_by_move = analyze["score_by_move"]
+       candidate = analyze["candidate"]
-       for x, y in self.mb.calc_legal_moves():
+       for move in self.mb.calc_legal_moves():
+           x, y = move
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
-           color = "red" if (x, y) in bestmoves else "black"
+           color = "red" if move in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
+           if score_by_move is not None:
+               color = "red" if move in candidate else "black"
+               ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
+           elif move in candidate:
+               ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui

なお、元のプログラムでは 10 行目for x, y in self.mb.calc_legal_moves(): と記述していましたが、着手を表すデータ を x 座標と y 座標にわけずに そのまま記述する必要がある処理 が 15、19、20、21 行目にあるため、上記の 10、11 行目のように修正しました。この修正を行うことで、例えば 15 行目で着手を記述する処理を元の color = "red" if (x, y) in bestmoves else "black" から、color = "red" if move in bestmoves else "black" のように記述することができます。

上記の修正後に下記のプログラムで gui_play() を実行して「分析」ボタンをクリックすると、実行結果の左図のように ai3s の候補手となる真ん中のマスに赤字の 1 が、それ以外のマスに黒字の 0 という ai3s が計算した評価値が表示される ようになります。また、真ん中のマスに着手すると、全てのマスが ai3s の候補手になるので、実行結果の右図のように全てのマスに赤字の 0 が表示されます。

gui_play()

実行結果

 

また、下記のプログラムの 3、4 行目のように、ai3 で計算した評価値を表示 するように update_gui を修正した後で、gui_play() を実行して「分析」ボタンをクリックすると、実行結果のように ai3 が計算した候補手のマスに、候補手という文字が表示される ことが確認できます。

1  def update_gui(self):
元と同じなので省略
2      if self.show_status:
3          from ai import ai3
4          bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
5          analyze = ai3(self.mb, analyze=True)
6          score_by_move = analyze["score_by_move"]
7        candidate = analyze["candidate"]
元と同じなので省略
8        
9  Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    def calc_status_txt(score):
        if score > 0:
            return ""
        elif score == 0:
            return ""
        else:
            return "×"
    
    ax = self.ax

    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()

    # y 軸を反転させる
    ax.invert_yaxis()

    # 枠と目盛りを表示しないようにする
    ax.axis("off")   

    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
            text += " " + calc_status_txt(score)
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=7*self.size)

    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
    if self.show_status:
        from ai import ai3
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        analyze = ai3(self.mb, analyze=True)
        score_by_move = analyze["score_by_move"]
        candidate = analyze["candidate"]
        for move in self.mb.calc_legal_moves():
            x, y = move
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if move in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
            if score_by_move is not None:
                color = "red" if move in candidate else "black"
                ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
            elif move in candidate:
                ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
                
    self.update_widgets_status()

    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
    if self.show_status:
-       from ai import ai3s
-       from ai import ai3
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
-       analyze = ai3s(self.mb, analyze=True)
+       analyze = ai3(self.mb, analyze=True)
        score_by_move = analyze["score_by_move"]
        candidate = analyze["candidate"]
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui
gui_play()

実行結果

 

今回の記事のまとめ

今回の記事では、ai3ai3s が、候補手の一覧と、それぞれの合法手を着手した場合に計算される評価値を返すことができるように修正し、ゲーム盤のマスに、そのマスに着手を行った際の ai3 の候補手や、ai3s の評価値を表示できるように修正しました。

現状では特定の AI の関数が計算した候補手または評価値しか表示できないので、次回の記事では他の AI の関数を修正し、マスに表示する評価値を計算する AI を切り替えることができるようにします。

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

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu_new.py 今回の記事で更新した marubatsu.py
ai_new.py 今回の記事で更新した ai.py
util_new.py 今回の記事で更新した util.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?