15

ミニマックス アルゴリズムを使用して無敵の tictactoe AI を作成しようとして 1 日を無駄にしました。途中で何かを逃しました(頭がおかしい)。

ここでコードを探しているわけではありません。どこが間違っていたのかをよりよく説明しているだけです。

これが私の現在のコードです(minimaxメソッドは何らかの理由で常に0を返します):

from copy import deepcopy


class Square(object):
    def __init__(self, player=None):
        self.player = player
    
    @property
    def empty(self):
        return self.player is None


class Board(object):
    winning_combos = (
        [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8],
        [0, 4, 8], [2, 4, 6],
    )
    
    def __init__(self, squares={}):
        self.squares = squares
        for i in range(9):
            if self.squares.get(i) is None:
                self.squares[i] = Square()
    
    @property
    def available_moves(self):
        return [k for k, v in self.squares.iteritems() if v.empty]
    
    @property
    def complete(self):
        for combo in self.winning_combos:
            combo_available = True
            for pos in combo:
                if not pos in self.available_moves:
                    combo_available = False
            if combo_available:
                return self.winner is not None
        return True
    
    @property
    def player_won(self):
        return self.winner == 'X'
    
    @property
    def computer_won(self):
        return self.winner == 'O'
    
    @property
    def tied(self):
        return self.complete == True and self.winner is None
    
    @property
    def winner(self):
        for player in ('X', 'O'):
            positions = self.get_squares(player)
            for combo in self.winning_combos:
                win = True
                for pos in combo:
                    if pos not in positions:
                        win = False
                if win:
                    return player
        return None
    
    @property
    def heuristic(self):
        if self.player_won:
            return -1
        elif self.tied:
            return 0
        elif self.computer_won:
            return 1
    
    def get_squares(self, player):
        return [k for k,v in self.squares.iteritems() if v.player == player]
    
    def make_move(self, position, player):
        self.squares[position] = Square(player)
    
    def minimax(self, node, player):
        if node.complete:
            return node.heuristic
        a = -1e10000
        for move in node.available_moves:
            child = deepcopy(node)
            child.make_move(move, player)
            a = max([a, -self.minimax(child, get_enemy(player))])
        return a


def get_enemy(player):
    if player == 'X':
        return 'O'
    return 'X'
4

3 に答える 3

19

ステップ 1: ゲーム ツリーを構築する

現在のボードから始めて、対戦相手が行うことができるすべての可能な動きを生成します。次に、それらのそれぞれに対して、実行できるすべての可能な動きを生成します。Tic-Tac-Toe の場合は、誰もプレイできなくなるまで続行します。他のゲームでは、通常、一定の時間または深さで停止します。

これは木のように見えます。紙に自分で描きます。現在のボードが一番上にあり、すべての対戦相手は 1 層下に移動し、対応するすべての可能な動きは 1 層下に移動します。

ステップ 2: ツリーの一番下にあるすべてのボードにスコアを付けます

Tic-Tac-Toe のような単純なゲームの場合、負けたらスコア 0、引き分け 50、勝ち 100 とします。

ステップ 3: スコアをツリーに伝播する

ここで、最小最大値の出番です。以前にスコアリングされていないボードのスコアは、その子と誰がプレイするかによって異なります。あなたと対戦相手の両方が、特定の状態で常に可能な限り最善の手を選択すると仮定します。対戦相手にとって最良の手は、あなたに最悪のスコアを与える手です。同様に、あなたの最高の動きは、最高のスコアを与える動きです。対戦相手のターンの場合、スコアが最小の子供を選択します (これにより、彼の利益が最大になります)。それがあなたの番であるならば、あなたは可能な限り最高の動きをするだろうと仮定するので、あなたは最大のものを選びます。

ステップ 4: 最善の手を選ぶ

次に、現在の位置から可能なすべてのプレイの中で最高の伝播スコアをもたらす動きをプレイします。

白紙のボードから始めるのが難しすぎて、高度な三目並べの位置から始める場合は、紙の上で試してみてください。

再帰の使用: 非常に多くの場合、これは再帰を使用して単純化できます。「スコアリング」関数は各深さで再帰的に呼び出され、深さが奇数か偶数かに応じて、すべての可能な移動に対してそれぞれ最大値または最小値が選択されます。移動が不可能な場合、ボードの静的スコアを評価します。再帰的なソリューション (サンプル コードなど) は、把握するのが少し難しい場合があります。

于 2010-10-18T03:01:07.357 に答える
7

すでにご存知のように、ミニマックスの考え方は、対戦相手が常に最悪の値で手をプレイすると仮定して、最良の値を深く検索することです (私たちにとって最悪なので、彼らにとっては最善です)。

アイデアは、各ポジションに価値を与えようとすることです。負けた位置は負の位置 (これは望ましくありません) であり、勝った位置は正です。あなたは常に最高値のポジションを狙うと仮定しますが、対戦相手は常に最低値のポジションを狙うと仮定します。これは、私たちにとって最悪の結果であり、相手にとっては最高の結果となります (相手が勝ち、負ける)。ですから、彼らの立場になって、彼らと同じようにできる限り良いプレーをしようとし、彼らがそうするだろうと想定します。
したがって、2 つの手が可能であることがわかった場合、1 つは勝つか負けるかの選択肢を与えるもので、もう 1 つはいずれにせよ引き分けになるものであり、あなたがそうさせた場合に彼らが勝つような動きをするだろうと想定します。だから抽選に行ったほうがいい。

次に、より「アルゴリズム的な」ビューに進みます。

2 つの可能な位置を除いて、グリッドがほぼ満杯であると想像してください。
あなたが最初のものをプレイしたときに何が起こるか考えてみてください:
相手はもう一方をプレイします。それが彼らの唯一の可能な動きなので、彼らからの他の動きを考慮する必要はありません. 結果を見て、結果の値を関連付けます (勝った場合は +∞、引き分けた場合は 0、負けた場合は -∞。三目並べの場合は +1 0 と -1 として表すことができます)。
次に、2 番目のものをプレイしたときに何が起こるかを考えてみましょう:
(ここでも同じことです。対戦相手には 1 つの手しかありません。結果の位置を見て、位置を評価します)。

2 つの動きから選択する必要があります。これは私たちの動きなので、最良の結果が必要です (これはミニマックスの「最大」です)。結果がより高いものを「最良の」手として選択します。以上、「エンドから2手」の例でした。

ここで、2 手ではなく 3 手が残っていると想像してください。
原則は同じです。3 つの可能な手のそれぞれに値を割り当てて、最適なものを選択できるようにします。
したがって、3 つの動きのいずれかを検討することから始めます。
あなたは今上記の状況にあり、可能な手は 2 つしかありませんが、相手の番です。次に、上で行ったように、対戦相手の可能な動きの 1 つを検討し始めます。同様に、可能な動きをそれぞれ見て、両方の結果値を見つけます。それは対戦相手の動きなので、彼らは彼らにとって「最良の」動きをプレイすると想定し、私たちにとって最悪のターンアウトを持っているので、それはより小さな値を持つものです (これはミニマックスの「最小」です)。もう一方は無視してください。いずれにせよ、あなたが彼らに最適だと思ったものを彼らが演奏すると仮定してください。これがあなたの手がもたらすものなので、3 つの手のうちの最初の手に割り当てる値です。

次に、他の可能な2つの動きをそれぞれ検討します。同じ方法で値を指定します。そして、あなたの 3 つの動きから、最大値を持つものを選択します。

次に、4 つの移動で何が起こるかを考えてみましょう。あなたの 4 つの動きのそれぞれについて、対戦相手の 3 つの動きに何が起こるかを見て、それらのそれぞれについて、残りの 2 つの動きの中で最良の結果をもたらすものを相手が選択すると仮定します。

これがどこに向かっているのかがわかります。最後から n ステップの動きを評価するには、n 個の可能な動きのそれぞれで何が起こるかを調べ、最良のものを選択できるように値を与えようとします。その過程で、n-1 でプレイするプレーヤー (対戦相手) にとって最良の手を見つけようとし、値が小さい方のステップを選択する必要があります。n-1 の動きを評価するプロセスでは、考えられる n-2 の動きから選択する必要があります。等。

これが、このアルゴリズムが本質的に再帰的である理由です。n が何であれ、ステップ n では、n-1 で可能なすべてのステップを評価します。すすいで繰り返します。

三目並べの場合、今日のマシンは、数百しかないため、ゲームの開始からすぐにすべての可能な結果を​​計算するのに十分強力です。より複雑なゲームに実装しようとすると、時間がかかりすぎるため、ある時点で計算を停止する必要があります。したがって、複雑なゲームの場合は、次の可能性のあるすべての動きを探し続けるか、現在の位置に値を与えて早期に戻るかを決定するコードも記述する必要があります。これは、最終的ではない位置の値を計算する必要があることも意味します。たとえば、チェスの場合、各対戦相手がボード上に持っている素材の量、メイトなしでチェックする可能性、コントロールするタイルの数、すべて、それは自明ではありません。

于 2010-10-18T02:59:31.447 に答える
3

完全な機能が期待どおりに機能していないため、何かが起こる前にゲームが引き分けであると宣言されます。たとえば、次の設定を検討してください。

>> oWinning = {
 1: Square('X'),
 3: Square('O'), 4: Square('X'),
 6: Square('O'), 8: Square('X'),
}
>> nb = Board(oWinning)
>> nb.complete
True
>> nb.tied
True

これは、次の動きでコンピューターの勝利になるはずです。代わりに、ゲームは引き分けだと言っています。

問題は、ロジックが完成した時点で、コンボ内のすべての正方形が空いているかどうかを確認することです。それらのいずれかがそうでない場合、そのコンボでは勝てないと推定されます。必要なことは、そのコンボのいずれかのポジションが占有されているかどうかを確認することです。それらのコンボがすべて「なし」または同じプレイヤーである限り、そのコンボはまだ使用可能であると見なされます。

例えば

def available_combos(self, player):
    return self.available_moves + self.get_squares(player)

@property
def complete(self):
    for player in ('X', 'O'):
        for combo in self.winning_combos:
            combo_available = True
            for pos in combo:
                if not pos in self.available_combos(player):
                   combo_available = False
            if combo_available:
                return self.winner is not None
    return True

更新されたコードでこれを適切にテストしたので、このテスト ケースで期待される結果が得られました。

>>> nb.minimax(nb, 'O')
-1
>>> nb.minimax(nb, 'X')
1
于 2010-10-18T12:28:33.560 に答える