ミニマックスアルゴリズムを使用して無敵の三目並べAIを作成しようとして、1日を無駄にしました。私は途中で何かを逃しました(脳揚げ)。
私はここでコードを探しているのではなく、どこが間違っていたのかをよりよく説明しています。
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'
完全な機能が期待どおりに機能していないため、何かが発生する前にゲームがタイであると宣言されます。たとえば、次の設定について考えてみます。
>> 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
ステップ1:ゲームツリーを構築する
現在のボードから始めて、対戦相手が行うことができるすべての可能な動きを生成します。次に、それらのそれぞれについて、あなたが行うことができるすべての可能な動きを生成します。 Tic-Tac-Toeの場合は、誰もプレイできなくなるまで続行します。他のゲームでは、通常、指定された時間または深さの後に停止します。
これは木のように見えます。自分で紙に描き、現在のボードを上に置きます。すべての対戦相手は1層下に移動し、すべての可能な移動は1層下に移動します。
ステップ2:ツリーの一番下にあるすべてのボードにスコアを付けます
Tic-Tac-Toeのような単純なゲームの場合、負けた場合はスコアを0にし、50で引き分け、100で勝ちます。
ステップ3:スコアをツリーに伝播します
ここで、ミニマックスが作用します。以前にスコアが付けられていなかったボードのスコアは、その子供と誰がプレイできるかによって異なります。あなたとあなたの対戦相手の両方が常に与えられた状態で可能な限り最良の動きを選択すると仮定します。対戦相手にとって最良の動きは、あなた最悪のスコアを与える動きです。同様に、あなたの最高の動きはあなたに最高のスコアを与える動きです。対戦相手のターンの場合、あなたは最小のスコア(彼の利益を最大化する)を持つ子供を選びます。それがあなたの番である場合、あなたはあなたが可能な限り最善の動きをするだろうと思います、それであなたは最大を選びます。
ステップ4:あなたの最良の動きを選んでください
次に、現在の位置から可能なすべてのプレイの中で最高の伝播スコアが得られる動きをプレイします。
空白のボードから始めるのが多すぎて、高度なTic-Tac-Toeの位置から始める場合は、紙で試してみてください。
再帰の使用:非常に多くの場合、これは再帰を使用することで簡略化できます。 「スコアリング」関数は、各深度で再帰的に呼び出され、深度が奇数であるかどうかに応じて、すべての可能な移動に対してそれぞれ最大または最小を選択します。移動が不可能な場合は、ボードの静的スコアを評価します。再帰的なソリューション(サンプルコードなど)は、把握するのが少し難しい場合があります。
すでにご存知のように、ミニマックスのアイデアは、対戦相手が常に最悪の値で移動すると仮定して、最良の値を深く検索することです(私たちにとっては最悪なので、彼らにとっては最高です)。
アイデアは、各位置に値を与えようとすることです。あなたが負ける位置は負であり(私たちはそれを望まない)、あなたが勝つ位置は正です。あなたは常に最高の価値のあるポジションを目指していると仮定しますが、対戦相手は常に最低の価値のポジションを目指します。これは私たちにとって最悪の結果であり、彼らにとって最高です(彼らは勝ち、私たちは負けます)。だからあなたは彼らの立場になって、彼らができる限り上手にプレーしようとし、彼らがそうするだろうと思い込んでください。
したがって、2つの動きが可能であることがわかった場合、1つは勝つか負けるかを選択でき、もう1つはとにかく引き分けになります。あなたが許可した場合、彼らは勝つための動きに行くと想定します。彼らはそれをします。だから引き分けに行く方がいいです。
次に、より「アルゴリズム的な」ビューを表示します。
2つの可能な位置を除いて、グリッドがほぼいっぱいになっていると想像してください。
最初のものをプレイするとどうなるか考えてみましょう:
対戦相手はもう一方をプレイします。それは彼らの唯一の可能な動きなので、彼らからの他の動きを考慮する必要はありません。結果を見て、結果の値を関連付けます(勝った場合は+∞、引き分けの場合は0、負けた場合は-∞:三目並べの場合は+1 0および-1として表すことができます)。
2番目のものをプレイするとどうなるか考えてみましょう:
(ここでも同じですが、対戦相手の動きは1つだけです。結果の位置を見て、位置を評価します)。
2つの動きから選択する必要があります。それは私たちの動きなので、最良の結果が必要です(これはミニマックスの「最大」です)。私たちの「最良の」動きとして、より高い結果を持つものを選択してください。 「終わりから2つの動き」の例は以上です。
ここで、2つではなく3つの動きが残っていると想像してください。
原則は同じです。3つの可能な動きのそれぞれに値を割り当てて、最適なものを選択できるようにします。
それで、あなたは3つの動きのうちの1つを考えることから始めます。
あなたは今、上記の状況にあり、可能な動きは2つだけですが、それは対戦相手の番です。次に、上記のように、対戦相手の可能な動きの1つを検討し始めます。同様に、可能な動きのそれぞれを見て、両方の結果値を見つけます。それは対戦相手の動きなので、彼らは彼らにとって「最高の」動き、私たちにとって最も悪い投票率の動きをするだろうと仮定します、それでそれはより小さな値(これはミニマックスの「最小」です)です。もう1つは無視してください;とにかく、あなたが見つけたものを彼らがプレイすることを想定してください。これはあなたの動きがもたらすものなので、3つの動きの最初に割り当てる値です。
ここで、他の可能な2つの動きのそれぞれを検討します。同じ方法でそれらに値を与えます。そして、3つの動きから、最大値を持つものを選択します。
ここで、4回の移動で何が起こるかを考えてみましょう。あなたの4つの動きのそれぞれについて、あなたは対戦相手の3つの動きに何が起こるかを見て、あなたは彼らがあなたのために残りの2つの動きの中で最良のものの可能な限り最悪の結果を与えるものを選ぶと仮定します。
これがどこに向かっているのかがわかります。最後からnステップの移動を評価するには、n個の可能な移動のそれぞれで何が起こるかを調べ、最良のものを選択できるように値を与えようとします。その過程で、n-1でプレイするプレイヤー、つまり対戦相手に最適な動きを見つけて、値の小さいステップを選択する必要があります。 n-1の動きを評価するプロセスでは、可能なn-2の動きから選択する必要があります。これは私たちのものであり、このステップでできる限りプレイすることを前提としています。等。
これが、このアルゴリズムが本質的に再帰的である理由です。 nが何であれ、ステップnで、n-1で可能なすべてのステップを評価します。すすぎ、繰り返します。
今日の三目並べの場合、マシンは数百しかないため、ゲームの開始からすぐにすべての可能な結果を計算するのに十分強力です。より複雑なゲームに実装しようとすると、時間がかかりすぎるため、ある時点でコンピューティングを停止する必要があります。したがって、複雑なゲームの場合は、考えられるすべての次の動きを探し続けるか、今すぐポジションに価値を与えて早く戻るかを決定するコードも作成する必要があります。つまり、最終的ではない位置の値も計算する必要があります。たとえば、チェスの場合、各対戦相手がボード上に持っている材料の量、相手がいない場合のチェックの即時の可能性、制御するタイルの数、すべて、それはそれを些細なことではありません。