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を一から作成する その132 デコレーター式を利用したすべての AI の関数の再定義

Last updated at Posted at 2024-11-12

目次と前回の記事

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

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

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

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

デコレーター式を利用したすべての AI の関数の再定義

前回までの記事で、デコレーター式を利用した ai2sai11s の定義を行いました。今回の記事では すべての AI の関数デコレーター式を利用して定義し直す ことにします。

ai2s の修正

前回の記事で、ラッパー関数の中で、ラップする関数を呼び出す処理 を、下記のプログラムの 4 行目のように、実引数に mbdebug を記述して呼び出す ように修正しました。

1  def ai_by_score(eval_func):
2      @wraps(eval_func)
3      def wrapper(mb_orig, debug=False, *args, rand=True, analyze=False, **kwargs):
省略
4              score = eval_func(mb, debug, *args, **kwargs)
省略
5      return wrapper

そのため、@ai_by_score というデコレーター式の 下に記述する関数の定義 では、必ず仮引数として mbdebug を持つ必要 があります。

しかし、以前の記事ai2s をデコレーター式で定義した際は、ai_by_score を上記のように 修正する前 であったため、下記のプログラムの 2 行目のように、仮引数には mb のみが記述 されています。

@ai_by_score
def ai2s(mb):
    return 0

そのため、下記のプログラムで ai2s を呼び出すと実行結果のように、エラーが発生 します。エラーメッセージは、ai2s に対して 1 つの位置引数を記述する必要があるのに対して、2 つの位置引数が記述されているという意味の表示が行われています。

from ai import ai2s
from marubatsu import Marubatsu

mb = Marubatsu()
print(ai2s(mb))

実行結果

略
File c:\Users\ys\ai\marubatsu\132\ai.py:169, in ai_by_score.<locals>.wrapper(mb_orig, debug, rand, analyze, *args, **kwargs)
    166 mb.move(x, y)
    167 dprint(debug, mb)
--> 169 score = eval_func(mb, debug, *args, **kwargs)
    170 dprint(debug, "score", score, "best score", best_score)
    171 if analyze:

TypeError: ai2s() takes 1 positional argument but 2 were given

ai2s を下記のプログラムのように、mbdebug の 2 つの仮引数を持つ ように定義する事で、この問題を解決することができます。下記のプログラムの 4、5 行目のラップする関数のように、debug の値を利用しない 場合でも 仮引数 debug を必ず記述 する必要があります。

  • 2 行目:仮引数 debug=False を追加する
1  from ai import ai_by_score
2
3  @ai_by_score
4  def ai2s(mb, debug=False):
5      return 0
行番号のないプログラム
from ai import ai_by_score

@ai_by_score
def ai2s(mb, debug=False):
    return 0
修正箇所
from ai import ai_by_score

@ai_by_score
-def ai2s(mb):
+def ai2s(mb, debug=False):
    return 0

上記の修正後に下記のプログラムを実行すると、エラーが発生しなくなったことが確認できます。

print(ai2s(mb))

実行結果(実行結果はランダムなので下記と異なる場合があります)

(2, 1)

ai2s の仮引数に関する補足

下記のプログラムのように、ラップする関数の仮引数debug の代わりに *args**kwargs を記述 するという方法もありますが、この方法はお勧めしません。なお、ai2s と区別するため、下記の関数の名前を ai2s_2 としました。

1  @ai_by_score
2  def ai2s_2(mb, *args, **kwargs):
3      return 0
行番号のないプログラム
@ai_by_score
def ai2s_2(mb, *args, **kwargs):
    return 0
修正箇所
@ai_by_score
-def ai2s(mb, debug=False):
+def ai2s_2(mb, *args, **kwargs):
    return 0

この方法で ai2s_2 を定義する事で、下記のプログラムのように ai2s_2 を呼び出す際に、2 つ目以降の実引数どのような値を記述 して呼び出しても エラーが発生しなくなります。なお、2 つ目の実引数に False を記述しているのは、ラッパー関数の仮引数 debugFalse を代入するためです。

ai2s_2(mb, False, 1, 2, a=3, b=4, c=5)

実行結果(実行結果はランダムなので下記と異なる場合があります)

(1, 1)

どのような実引数を記述してもエラーにならないこと は、メリットだと思う人がいるかもしれませんが、これは 大きなデメリット になります。

例えば、ai2s では、下記のプログラムのように、キーワード引数 debug の綴りdebig のように間違って記述 して呼び出すと、実行結果のように予期しないキーワード引数が記述されたことが原因の エラーが発生 します。エラーが発生することで、プログラムが停止する ため、何かがおかしいことに必ず気づくことができます

print(ai2s(mb, debig=True))

実行結果

略
File c:\Users\ys\ai\marubatsu\132\ai.py:169, in ai_by_score.<locals>.wrapper(mb_orig, debug, rand, analyze, *args, **kwargs)
    166 mb.move(x, y)
    167 dprint(debug, mb)
--> 169 score = eval_func(mb, debug, *args, **kwargs)
    170 dprint(debug, "score", score, "best score", best_score)
    171 if analyze:

TypeError: ai2s() got an unexpected keyword argument 'debig'

一方で、下記のプログラムのように、同じ 間違ったキーワード引数を記述 して ai2s_s を呼び出しても 実行結果のように エラーは発生しません が、本来行いたかった debug=True を記述した際の デバッグ表示も行われません。エラーが発生しないため、プログラムの処理が続行されるため間違った処理 が行われていることに 気が付かない可能性が高くなります

print(ai2s_2(mb, debig=True))

実行結果(実行結果はランダムなので下記と異なる場合があります)

(1, 2)

このように、必要のない実引数を記述してもエラーにならないこと は、メリットよりも デメリットの方が多い ため、必要がない場合 に仮引数に *args**kwargs記述しないほうが良い でしょう。

*args**kwargs が必要となる具体例 としては以下のような例が挙げられます。

  • 任意の仮引数を持つ関数 に対する ラッパー関数
  • print のように、任意の数の実引数 に対して処理を行う関数

ai1s の修正

ai1sルール 1の、「左上から順に空いているマスを探し、最初に見つかったマスに着手 する」という方法で着手を選択しますが、下記のプログラムのように評価値を計算する eval_func は常に 0 を返しai_by_score の実引数に rand=False を記述 することで、乱数を使わずに 最初の合法手を選択する という、特殊な処理を行っています。

def ai1s(mb, debug=False):
    def eval_func(mb):
        return 0

    return ai_by_score(mb, eval_func, debug=debug, rand=False)

そのため、上記の eval_func に対して @ai_by_score のデコレーター式を記述して ai1s を定義 すると、ランダムな着手を行う ai2s と同じ処理を行う関数が定義 されてしまします。

@ai_by_score のデコレーター式を利用して ルール 1 の ai1s を定義 するためには、直前の着手 に対して、左上のマスから 右方向に 順番に小さくなっていく ような評価値を計算する関数を定義する必要があります。具体的な評価値として、本記事では下記のプログラムのように、直前の着手 に対して 左上のマスから順に 87、・・・、0 という評価値を計算する関数を定義することにします。

  • 3、4 行目:ゲーム開始時の局面は、直前の着手が存在しないので、その場合は評価値として 0 を返すようにする
  • 5 ~ 7 行目:直前の着手を (x, y) とすると x + y * 3 という式で、以前の記事で説明した、下図のように左上のマスから順に 0 ~ 8 が割り当てられた 数値座標 を計算できる。8 から数値座標を引き算する ことで、求める評価値を計算することができる
1  @ai_by_score
2  def ai1s(mb, debug=False):
3      if mb.last_move is None:
4          return 0
5      else:
6          x, y = mb.last_move
7          return 8 - (x + y * 3)
行番号のないプログラム
@ai_by_score
def ai1s(mb, debug=False):
    if mb.last_move is None:
        return 0
    else:
        x, y = mb.last_move
        return 8 - (x + y * 3)

上記の定義後に下記のプログラムを実行すると、ゲーム開始時の局面に対して、ai1s が左上の (0, 0) を選択することが確認できます。

print(ai1s(mb))

実行結果

(0, 0)

また、下記のプログラムで実引数に analyze=True を記述して ai1s を呼び出すと、実行結果のように 左上のマスから順に 87、・・・、0 という評価値が計算されている ことが確認できます。なお、pprintdict の要素 を登録した順ではなく、キーの値によって並べ替えて表示する ので、下記のようにキーの値が 左上のマスから縦方向の順番 で表示され、実行結果では評価値が 8、5、2・・・の順で表示されます。

from pprint import pprint

pprint(ai1s(mb, analyze=True))

実行結果

{'candidate': [(0, 0)],
 'score_by_move': {(0, 0): 8,
                   (0, 1): 5,
                   (0, 2): 2,
                   (1, 0): 7,
                   (1, 1): 4,
                   (1, 2): 1,
                   (2, 0): 6,
                   (2, 1): 3,
                   (2, 2): 0}}

下記のプログラムのように print で表示 すれば 登録した順 で dict の要素が表示されます。

print(ai1s(mb, analyze=True))

実行結果

{'candidate': [(0, 0)], 'score_by_move': {(0, 0): 8, (1, 0): 7, (2, 0): 6, (0, 1): 5, (1, 1): 4, (2, 1): 3, (0, 2): 2, (1, 2): 1, (2, 2): 0}}

残りの評価値を利用して着手を選択する AI の再定義

ここまでで、評価値を利用して着手を選択する AI のうち、ai1sai2sai11s をデコレーター式を利用して定義しなおしました。残り の、ai3s ~ ai10sai10s ~ ai14s に対しても 同様の方法@ai_by_score のデコレーター式を 利用して定義し直す ことができます。

長くなるので折りたたみますが、下記のプログラムが ai1s ~ ai14s@ai_by_score のデコレーター式を利用して定義しなおしたものです。

ai1s ~ ai14s の再定義
from marubatsu import Markpat

@ai_by_score
def ai1s(mb, debug=False):
    if mb.last_move is None:
        return 0
    else:
        x, y = mb.last_move
        return 8 - (x + y * 3)

@ai_by_score
def ai2s(mb, debug=False):
    return 0    
    
@ai_by_score
def ai3s(mb, debug=False):
    if mb.last_move == (1, 1):
        return 1
    else:
        return 0
    
@ai_by_score
def ai4s(mb, debug=False):
    x, y = mb.last_move
    if mb.last_move == (1, 1):
        return 10
    elif x % 2 == 0 and y % 2 == 0:
        return 9 - (x + y * 3)
    else:
        return 0
    
@ai_by_score
def ai5s(mb, debug=False):
    if mb.status == mb.last_turn:
        return 1
    else:
        return 0

@ai_by_score
def ai6s(mb, debug=False):
    # 自分が勝利している場合は、評価値として 1 を返す
    if mb.status == mb.last_turn:
        return 1

    # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
    # 横方向と縦方向の判定
    for i in range(mb.BOARD_SIZE):
        count = mb.count_marks(coord=[0, i], dx=1, dy=0)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
        count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
    # 左上から右下方向の判定
    count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
    if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
        return -1
    # 右上から左下方向の判定
    count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
    if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
        return -1

    # それ以外の場合は評価値として 0 を返す
    return 0

@ai_by_score
def ai7s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 2 を返す
    if mb.last_move == (1, 1):
        return 2

    # 自分が勝利している場合は、評価値として 1 を返す
    if mb.status == mb.last_turn:
        return 1

    # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
    # 横方向と縦方向の判定
    for i in range(mb.BOARD_SIZE):
        count = mb.count_marks(coord=[0, i], dx=1, dy=0)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
        count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
    # 左上から右下方向の判定
    count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
    if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
        return -1
    # 右上から左下方向の判定
    count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
    if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
        return -1

    # それ以外の場合は評価値として 0 を返す
    return 0

@ai_by_score
def ai8s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 3 を返す
    if mb.last_move == (1, 1):
        return 3

    # 自分が勝利している場合は、評価値として 2 を返す
    if mb.status == mb.last_turn:
        return 2

    markpats = mb.enum_markpats()
    # 相手が勝利できる場合は評価値として -1 を返す
    if Markpat(last_turn=0, turn=2, empty=1) in markpats:
        return -1
    # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
    elif Markpat(last_turn=2, turn=0, empty=1) in markpats:
        return 1
    # それ以外の場合は評価値として 0 を返す
    else:
        return 0
    
@ai_by_score
def ai9s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 4 を返す
    if mb.last_move == (1, 1):
        return 4

    # 自分が勝利している場合は、評価値として 3 を返す
    if mb.status == mb.last_turn:
        return 3

    markpats = mb.count_markpats()
    if debug:
        pprint(markpats)
    # 相手が勝利できる場合は評価値として -1 を返す
    if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
        return -1
    # 次の自分の手番で自分が必ず勝利できる場合は評価値として 2 を返す
    elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
        return 2
    # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
    elif markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
        return 1
    # それ以外の場合は評価値として 0 を返す
    else:
        return 0
    
@ai_by_score
def ai10s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 300 を返す
    if mb.last_move == (1, 1):
        return 300

    # 自分が勝利している場合は、評価値として 200 を返す
    if mb.status == mb.last_turn:
        return 200

    markpats = mb.count_markpats()
    if debug:
        pprint(markpats)
    # 相手が勝利できる場合は評価値として -100 を返す
    if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
        return -100
    # 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
    elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
        return 100

    # 評価値の合計を計算する変数を 0 で初期化する
    score = 0        
    # 次の自分の手番で自分が勝利できる場合は評価値に 1 を加算する
    if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
        score += 1
    # 「自 1 敵 0 空 2」の数だけ、評価値を加算する
    score += markpats[Markpat(last_turn=1, turn=0, empty=2)]
    
    # 計算した評価値を返す
    return score

@ai_by_score
def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):
    # 真ん中のマスに着手している場合は、評価値として 300 を返す
    if mb.last_move == (1, 1):
        return 300

    # 自分が勝利している場合は、評価値として 200 を返す
    if mb.status == mb.last_turn:
        return 200

    markpats = mb.count_markpats()
    if debug:
        pprint(markpats)
    # 相手が勝利できる場合は評価値として -100 を返す
    if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
        return -100
    # 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
    elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
        return 100

    # 評価値の合計を計算する変数を 0 で初期化する
    score = 0        
    # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
    if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
        score += score_201
    # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
    score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
    # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
    score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
    
    # 計算した評価値を返す
    return score

@ai_by_score
def ai12s(mb, debug=False, score_victory=300, score_sure_victory=200, \
          score_defeat=-100, score_special=100, score_201=2, \
          score_102=0.5, score_012=-1):
    # 自分が勝利している場合
    if mb.status == mb.last_turn:
        return score_victory

    markpats = mb.count_markpats()
    if debug:
        pprint(markpats)
    # 相手が勝利できる場合
    if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
        return score_defeat
    # 次の自分の手番で自分が必ず勝利できる場合
    elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
        return score_sure_victory
    
    # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
    if mb.board[1][1] == Marubatsu.CROSS and \
        (mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
        mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
        (mb.board[1][0] == Marubatsu.CROSS or \
        mb.board[0][1] == Marubatsu.CROSS or \
        mb.board[2][1] == Marubatsu.CROSS or \
        mb.board[1][2] == Marubatsu.CROSS) and \
        mb.move_count == 4:
        return score_special    

    # 評価値の合計を計算する変数を 0 で初期化する
    score = 0        
    # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
    if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
        score += score_201
    # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
    score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
    # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
    score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
    
    # 計算した評価値を返す
    return score    

@ai_by_score
def ai13s(mb, debug=False, score_victory=300, score_sure_victory=200, \
          score_defeat=-100, score_special=100, score_201=2, \
          score_102=0.5, score_012=-1):
    # 自分が勝利している場合
    if mb.status == mb.last_turn:
        return score_victory

    markpats = mb.count_markpats()
    if debug:
        pprint(markpats)
    # 相手が勝利できる場合
    if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
        return score_defeat * markpats[Markpat(last_turn=0, turn=2, empty=1)]
    # 次の自分の手番で自分が必ず勝利できる場合
    elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
        return score_sure_victory
    
    # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
    if mb.board[1][1] == Marubatsu.CROSS and \
        (mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
        mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
        (mb.board[1][0] == Marubatsu.CROSS or \
        mb.board[0][1] == Marubatsu.CROSS or \
        mb.board[2][1] == Marubatsu.CROSS or \
        mb.board[1][2] == Marubatsu.CROSS) and \
        mb.move_count == 4:
        return score_special    

    # 評価値の合計を計算する変数を 0 で初期化する
    score = 0        
    # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
    if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
        score += score_201
    # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
    score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
    # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
    score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
    
    # 計算した評価値を返す
    return score

@ai_by_score
def ai14s(mb, debug=False, score_victory=300, score_sure_victory=200, \
          score_defeat=-100, score_special=100, score_201=2, \
          score_102=0.5, score_012=-1):
    # 評価値の合計を計算する変数を 0 で初期化する
    score = 0        

    # 自分が勝利している場合
    if mb.status == mb.last_turn:
        return score_victory

    markpats = mb.count_markpats()
    if debug:
        pprint(markpats)
    # 相手が勝利できる場合は評価値を加算する
    if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
        score = score_defeat * markpats[Markpat(last_turn=0, turn=2, empty=1)]
    # 次の自分の手番で自分が必ず勝利できる場合
    elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
        return score_sure_victory
    
    # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
    if mb.board[1][1] == Marubatsu.CROSS and \
        (mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
        mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
        (mb.board[1][0] == Marubatsu.CROSS or \
        mb.board[0][1] == Marubatsu.CROSS or \
        mb.board[2][1] == Marubatsu.CROSS or \
        mb.board[1][2] == Marubatsu.CROSS) and \
        mb.move_count == 4:
        return score_special    

    # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
    if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
        score += score_201
    # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
    score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
    # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
    score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
    
    # 計算した評価値を返す
    return score

なお、再定義した AI の関数が 正しく動作するか どうかの 検証は次回の記事 で行います。

評価値を計算しない AI の関数に対するデコレーター式による再定義

下記のプログラムの ai2 などの、評価値を計算せずに着手の選択を行う AI の関数 に対しては、ai_by_score を利用 してラッパー関数を 定義する事はできません

def ai2(mb):
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)

評価値を計算しない AI に対するラッパー関数 は、ai_by_score と同様に、ラップする AI の関数に対して、下記の機能を追加する必要 があります。

  • デバッグ表示を行うことを表す仮引数 debug を追加する
  • ランダムな着手を行うかどうかを表す仮引数 rand を追加する
  • 候補手の一覧1を返り値として返すことを表す仮引数 analyze を追加する

この中で、仮引数 analyze を追加 するという 機能の拡張 は、以前の記事で下記の ai3 に対して既に行っています

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

上記のプログラムに対して、下記のように修正 することで、仮引数 analyze に代入された値によって候補手の一覧を返り値として返すように ai3 の機能を拡張 しています。

 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)

そこで、上記の修正を参考 に、最初に 仮引数 analyze に関する機能を追加 したラッパー関数を作成する デコレーターの関数を定義 する事にし、その後で仮引数 debugrand の機能を追加したデコレーターの関数を定義する事にします。

仮引数 analyze の機能の追加

上記のプログラムの 6 ~ 12 行目ai3 に対して、仮引数 analyze の処理を追加した部分 です。従って、ラッパー関数を定義して ai3 の機能の拡張を行う場合は、この部分の処理をラッパー関数の中に記述 します。

具体的には、デコレータの関数を以下のように定義します。なお、デコレーターの関数の名前を ai_by_candidate とした理由はこの後で説明します。

  • 6 行目:仮引数 mb*argsanalyze**args を持つラッパー関数を定義する。ラップする関数が任意の仮引数を持つことができるように 仮引数 *args**kwargs を記述 する。また、仮引数 analyzeラッパー関数のみで使われる仮引数 ので、*args より後で記述する
  • 7 行目:ラップする関数を呼び出し、返り値を candidate に代入する
  • 8 ~ 14 行目ai3 の機能を拡張する、上記の 6 ~ 12 行目と同じ処理を行う
 1  from functools import wraps
 2  from random import choice
 3
 4  def ai_by_candidate(func):
 5      @wraps(eval_func)
 6      def wrapper(mb, *args, analyze=False, **kwargs):
 7          candidate = func(mb, *args, **kwargs)
 8          if analyze:
 9              return {
10                  "candidate": candidate,
11                  "score_by_move": None
12              }
13          else:
14              return choice(candidate)
15       
16      return wrapper
行番号のないプログラム
from functools import wraps
from random import choice

def ai_by_candidate(func):
    @wraps(eval_func)
    def wrapper(mb, *args, analyze=False, **kwargs):
        candidate = func(mb, *args, **kwargs)
        if analyze:
            return {
                "candidate": candidate,
                "score_by_move": None
            }
        else:
            return choice(candidate)
        
    return wrapper

@ai_by_candidate のデコレーター式を利用して、ai3 を下記のプログラムのように定義し直すことができます。上記のプログラムの ラッパー関数の 6 行目ラップする関数を呼び出し返り値として候補手の一覧を受け取っている ので、下記のプログラムのように 7 行目で 候補手の一覧を表す candidate を返す処理を記述 する必要があります。

1  @ai_by_candidate
2  def ai3(mb):
3      if mb.board[1][1] == Marubatsu.EMPTY:
4          candidate = [(1, 1)]
5      else:
6          candidate = mb.calc_legal_moves()
7      return candidate
行番号のないプログラム
@ai_by_candidate
def ai3(mb):
    if mb.board[1][1] == Marubatsu.EMPTY:
        candidate = [(1, 1)]
    else:
        candidate = mb.calc_legal_moves()
    return candidate

上記の実行後に下記のプログラムを実行することで、実行結果から ai3 が正しく動作することが確認できます。なお、ai3真ん中のマスに優先的に着手を行う ルール 3 を実装した AI なので、ゲーム開始時の局面では必ず (1, 1) に着手 を行います。

print(ai3(mb))

実行結果

(1, 1)

また、下記のプログラムで実引数に analyze=True を記述して ai3 を呼び出した場合も、実行結果から正しく動作することが確認できます。

print(ai3(mb, analyze=True))

実行結果

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

@ai_by_candidate を利用した ai2 の定義

@ai_by_candidate を利用した ai2 を定義する際に、下記のプログラムのように 元の ai2 の定義をそのまま記述 することは できません

@ai_by_candidate
def ai2(mb):
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)

上記の実行後に下記のプログラムを実行すると、実行結果のように座標ではなく、0 などの数値が表示 されます。なお、表示される数値は 0、1、2 の中からランダムに選ばれます

print(ai2(mb))

実行結果(実行結果はランダムなので下記と異なる場合があります)

0

このようなことが起きるのは、ai_by_candidate が定義する ラッパー関数 が、ラップする関数の返り値 として 候補手の一覧を必要とする からです。上記の ai2ラップする関数 は、候補手の一覧ではなく、候補手の中からランダムに選択した 1 つの合法手を返します。例えば (0, 1) という合法手が 返り値として得られた場合 は、ai_by_candidatewrapper の中で choice((0, 1)) が返り値として返されるので、0 または 1いずれかの数値が返ります。このように、上記の ai2 は、合法手x 座標または y 座標のどちらかを返り値として返す という間違った処理を行います。

従って、ai_by_candidate をデコレーターとして利用する場合は、ラップする関数 が返り値として 候補手の一覧のデータを返す 必要があります。下記は、そのように ai2 を定義したプログラムです。なお、デコレーターの関数の名前ai_by_candidate としたのは、候補手(candidate)を返す関数に対する AI のラッパー関数を作成 する処理を行うからです。

@ai_by_candidate
def ai2(mb):
    return mb.calc_legal_moves()

上記のように ai2 を修正することで、下記のプログラムのように ai2 が正しい処理を行うようになったことが確認できます。

print(ai2(mb))

実行結果(実行結果はランダムなので下記と異なる場合があります)

(1, 1)

仮引数 debugrand の機能の追加

次に、ai_by_candidate 対して 仮引数 debugrand の機能を追加 することにします。debugTrue が代入 されていた場合は、候補手の一覧を表示 することにします。

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

  • 5 行目:ラッパー関数の仮引数 *args の前に仮引数 debug を、後に rand を追加する
  • 6 行目ay_by_score の場合と同様に、ラップする関数を呼び出す際に、2 つ目の実引数に debug を記述するように修正する
  • 7 行目dprint を使って、debugTrue の場合に候補手の一覧を表示する
  • 14 ~ 17 行目randTrue の場合は候補手の中からランダムに選択した合法手を返し、False の場合は先頭の候補手を返すように修正する
 1  from ai import dprint
 2
 3  def ai_by_candidate(func):
 4      @wraps(func)
 5      def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
 6          candidate = func(mb, debug, *args, **kwargs)
 7          dprint(debug, "candidate", candidate)
 8          if analyze:
 9              return {
10                  "candidate": candidate,
11                  "score_by_move": None
12              }
13          else:
14              if rand:
15                  return choice(candidate)
16              else:
17                  return candidate[0]
18        
19      return wrapper
行番号のないプログラム
from functools import wraps
from ai import dprint

def ai_by_candidate(func):
    @wraps(func)
    def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
        candidate = func(mb, debug, *args, **kwargs)
        dprint(debug, "candidate", candidate)
        if analyze:
            return {
                "candidate": candidate,
                "score_by_move": None
            }
        else:
            if rand:
                return choice(candidate)
            else:
                return candidate[0]
        
    return wrapper
修正箇所
from ai import dprint

def ai_by_candidate(func):
    @wraps(func)
-   def wrapper(mb, *args, analyze=False, **kwargs):
+   def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
-       candidate = func(mb, *args, **kwargs)
+       candidate = func(mb, debug, *args, **kwargs)
+       dprint(debug, "candidate", candidate)
        if analyze:
            return {
                "candidate": candidate,
                "score_by_move": None
            }
        else:
-           return choice(candidate)
+           if rand:
+               return choice(candidate)
+           else:
+               return candidate[0]
        
    return wrapper

上記の修正後に、下記のプログラムで ai2 を定義し直します。ラップする関数を呼び出す際に 2 つ目の実引数に debug を記述する ように修正したので、2 行目では仮引数 debug を追加する 必要があります。

1  @ai_by_candidate
2  def ai2(mb, debug=False):
3      return mb.calc_legal_moves()
行番号のないプログラム
@ai_by_candidate
def ai2(mb, debug=False):
    return mb.calc_legal_moves()
修正箇所
@ai_by_candidate
-def ai2(mb):
+def ai2(mb, debug=False):
    return mb.calc_legal_moves()

上記の修正後に下記のプログラムで debug=Truerand=False を記述して ai2 を呼び出すと、実行結果のように 候補手の一覧が表示 され、必ず最初の候補手である (0, 0) が返り値として返るようになります。何度か下記のプログラムを実行して常に (0, 0) が返るようになったことを確認して下さい。

print(ai2(mb, rand=False, debug=True))

実行結果

candidate [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
(0, 0)

残りの評価値を利用しない AI の再定義

長くなるので折りたたみますが、下記のプログラムが ai1 ~ ai7ai_gt1 ~ ai_gt6@ai_by_candidate のデコレーター式を利用して定義しなおしたものです。

その際に、先ほど説明したように、修正前の関数に対して、返り値として候補手の一覧を返す ように修正する必要があります。また、元のプログラムが return 1, 1 のように、一つの合法手を返す場合 は、return [(1, 1)] のように、合法手を 1 つだけ持つ list の形式で返す ように修正する必要がある点に注意して下さい。

ai1 ~ ai7ai_gt1 ~ ai_gt6 の再定義
from copy import deepcopy

@ai_by_candidate
def ai1(mb, debug=False):
    for y in range(mb.BOARD_SIZE):
        for x in range(mb.BOARD_SIZE):
            if mb.board[x][y] == Marubatsu.EMPTY:
               return [(x, y)]
           
@ai_by_candidate
def ai2(mb, debug=False):
    legal_moves = mb.calc_legal_moves()
    return legal_moves

@ai_by_candidate
def ai3(mb, debug=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        candidate = [(1, 1)]
    else:
        candidate = mb.calc_legal_moves()
    return candidate

@ai_by_candidate
def ai4(mb, debug=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        return [(1, 1)]
    for y in range(0, 3, 2):
        for x in range(0, 3, 2):
            if mb.board[x][y] == Marubatsu.EMPTY:
                return [(x, y)]
    return mb.calc_legal_moves()

@ai_by_candidate
def ai5(mb_orig, debug=False):
    legal_moves = mb_orig.calc_legal_moves()
    # すべての合法手について繰り返し処理を行う
    for move in legal_moves:
        # mb_orig をコピーし、コピーしたもの対して着手を行う
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        # 勝利していれば、その合法手を返り値として返す
        if mb.status == mb_orig.turn:
            return [move]
    return legal_moves

@ai_by_candidate
def ai6(mb_orig, debug=False):
    # mb_orig の合法手の中で、自分が勝利できる合法手があればそこに着手する
    legal_moves = mb_orig.calc_legal_moves()
    # 合法手が 1 つしかない場合は、その合法手を返り値として返す
    if len(legal_moves) == 1:
        return legal_moves
    # 合法手の中で、勝てるマスがあれば、その合法手を返り値として返す
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        if mb.status == mb_orig.turn:
            return [move]
    # 〇 が勝利する合法手が存在しないことが確定した場合は、
    # 現在の局面を相手の手番とみなし、合法手の中で、相手が着手して
    # 勝利するマスがあれば、その合法手を返り値として返す
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        # 現在の局面の手番を入れ替える
        mb.turn = Marubatsu.CROSS if mb.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
        enemy_turn = mb.turn
        x, y = move
        mb.move(x, y)
        if mb.status == enemy_turn:
            return [move]
       
    return legal_moves

@ai_by_candidate
def ai7(mb, debug=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        return [(1, 1)]
    return ai6(mb, debug=debug, analyze=True)["candidate"]

@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree=None):
    node = mbtree.root
    for move in mb.records[1:]:
        node = node.children_by_move[move]

    bestmoves = []
    for move, childnode in node.children_by_move.items():
        if node.score == childnode.score:
            bestmoves.append(move)
    return bestmoves
    
@ai_by_candidate
def ai_gt2(mb, debug=False, mbtree=None):
    node = mbtree.root
    for move in mb.records[1:]:
        node = node.children_by_move[move]

    return node.bestmoves

@ai_by_candidate
def ai_gt3(mb, debug=False, mbtree=None):
    node = mbtree.nodelist_by_mb[tuple(mb.records)]
    return node.bestmoves    

@ai_by_candidate
def ai_gt4(mb, debug=False, mbtree=None):
    return mbtree.bestmoves_by_mb[tuple(mb.records)]

@ai_by_candidate
def ai_gt5(mb, debug=False, bestmoves=None):
    return bestmoves[tuple(mb.records)]

@ai_by_candidate
def ai_gt6(mb, debug=False, bestmoves_by_board=None):
    return bestmoves_by_board[mb.board_to_str()]    

いくつかの関数の再定義について補足します。

ai7 の再定義

修正前の ai7 は下記のプログラムのように、その中で ai6 を呼び出した返り値を返しています が、ai6 の返り値は候補手の一覧ではない ので、この関数の前に @ai_by_candidate のデコレーター式を記述 しても うまくいきません

def ai7(mb):
    if mb.board[1][1] == Marubatsu.EMPTY:
        return 1, 1
    return ai6(mb)

そのため、@ai_by_candidate のデコレーター式で ai7 を定義し直す 場合は、下記のプログラムの 5 行目のように 候補手の一覧を返すように修正 する必要があります。

  • 5 行目:実引数に analyze=True を記述して ai6 を呼び出すことで、ai6 の返り値の中に候補手の一覧が含まれる ように修正する。また、その 返り値の dictcandidate というキーの値に 候補手の一覧が代入 されているので、そのキーの値を返り値として返す ように修正する
1  @ai_by_candidate
2  def ai7(mb, debug=False):
3      if mb.board[1][1] == Marubatsu.EMPTY:
4          return [(1, 1)]
5      return ai6(mb, debug=debug, analyze=True)["candidate"]

ai_gt1 ~ ai_gt6 の再定義

修正前の ai_gt1 ~ ai_gt6 は、下記のプログラムのように mb 以外の仮引数 を持ちます。

def ai_gt1(mb, mbtree):

@ai_by_candidate のデコレーター式 を利用する場合は、2 つ目の仮引数に debug を記述 する必要がありますが、下記のプログラムのように 仮引数 debug をデフォルト引数として定義 すると、デフォルト引数より後通常の仮引数 mbtree が定義 されているため、エラーが発生 します。

@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree):

このような場合は、下記のプログラムの mbtree=None のように、仮引数 mbtree に何らかのデフォルト値を設定 する必要があります。ai_gt1 の処理では mbtree に適切なデータが代入されている必要があるため、mbtree に対応する実引数を記述せずに ai_gt1 を呼び出すと、ai_gt1 の中の処理でエラーが発生します。

@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree=None):

下記のプログラムのように、debug をデフォルト引数としないようにするという方法もありますが、この場合は ai_gt1 を呼び出す際に、debug に対する実引数を省略できなくなる点が不便 になります。

@ai_by_candidate
def ai_gt1(mb, debug, mbtree):

再定義した AI の関数の検証

再定義した AI の関数 正しく動作するか を、それぞれの AI の関数を呼び出して検証するのは、AI の数が多いため大変です。本記事では他の検証方法として、gui_play で AI を選択して AI どうしの対戦を行う ことで 検証する ことにします。ただし、gui_play では、ai.py の中で定義されている AI の関数 を使って AI の対戦を行っています。現状の ai.py には修正前の AI の関数が定義 されているので、このままでは gui_play を利用して、再定義した AI の関数の検証を行うことはできません。

今回の記事で再定義した AI の関数 は ai_new.py に記述し、その内容は 次回の記事の ai.py に反映する ので、再定義した AI の 関数の検証は次回の記事で行う ことにします。

今回の記事の内容

今回の記事では、評価値を利用 して着手を選択する AI の関数 をデコレーター式 @ai_by_score を利用して再定義 しました。

また、評価値を利用せず に着手を選択する AI の関数 に対する デコレータの関数 ay_by_candidate を定義 し、評価値を利用せずに着手を選択する AI の関数を デコレーター式 @ai_by_candidate を利用して再定義 しました。

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

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

次回の記事

  1. ai_by_score が定義するラッパー関数では、候補手の一覧だけでなく、それぞれの合法手を着手した場合の局面の評価値の一覧も返しますが、評価値を計算しない AI の場合はそのデータを計算することは不可能なので、以前の記事で説明したように、そのデータには None を設定します

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?