Минимакс объяснил для идиота - PullRequest
14 голосов
/ 18 октября 2010

Я потратил впустую весь свой день, пытаясь использовать минимаксный алгоритм для создания непобедимого тиктактого ИИ. По дороге я что-то пропустил (мозги жареные).

Я не ищу здесь код, просто лучшее объяснение того, где я ошибся.

Вот мой текущий код (метод минимакса всегда возвращает 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'

Ответы [ 3 ]

18 голосов
/ 18 октября 2010

Шаг 1: Постройте свое игровое дерево

Начиная с текущей доски, генерируйте все возможные ходы, которые может сделать ваш противник. Затем для каждого из них создайте все возможные ходы, которые вы можете сделать. Для Tic-Tac-Toe просто продолжайте, пока никто не сможет играть. В других играх вы обычно останавливаетесь после определенного времени или глубины.

Это похоже на дерево, нарисуйте его на листе бумаги, текущую доску вверху, все противники перемещаются на один уровень ниже, все ваши возможные ходы в ответ на один уровень ниже и т. Д.

Шаг 2: Оценка всех досок в нижней части дерева

Для такой простой игры, как крестики-нолики, наберите 0, если проиграете, 50 ничьих, 100 побед.

Шаг 3: Распространить счет вверх по дереву

Это где мин-макс вступает в игру. Оценка ранее не оцененной доски зависит от ее детей и того, кто будет играть. Предположим, что и вы, и ваш оппонент всегда выбираете лучший ход в данном состоянии. Лучший ход противника - ход, который дает you худший результат. Кроме того, ваш лучший ход - это ход, который дает вам наивысший балл. В случае хода противника вы выбираете ребенка с минимальным количеством очков (что максимизирует его выгоду). Если это ваша очередь, вы предполагаете, что сделаете лучший ход, поэтому выбираете максимум.

Шаг 4: выберите лучший ход

Теперь сыграйте ход, в результате которого будет получен лучший результат среди всех ваших возможных игр с текущей позиции.

Попробуйте это на листе бумаги, если начинать с пустой доски слишком много, чтобы начать с какой-то продвинутой позиции крестики-нолики.

Использование рекурсии: Очень часто это можно упростить с помощью рекурсии. Функция «скоринг» вызывается рекурсивно на каждой глубине и в зависимости от того, является ли глубина нечетной или четной, она выбирает макс или мин соответственно для всех возможных ходов. Когда никакие шаги не возможны, он оценивает статическую оценку доски. Рекурсивные решения (например, пример кода) могут быть немного сложнее понять.

7 голосов
/ 18 октября 2010

Как вы уже знаете, идея Minimax заключается в глубоком поиске наилучшего значения, предполагая, что противник всегда будет играть ход с наихудшим значением (худшим для нас, поэтому наилучшим для них).

По идее, вы будете пытаться дать значение каждой позиции.Позиция, в которой вы проигрываете, отрицательна (мы не хотим этого), а позиция, в которой вы выигрываете, является положительной.Вы предполагаете, что всегда будете пытаться получить позицию с наивысшей ценностью, но вы также предполагаете, что противник всегда будет стремиться к позиции с наименьшей ценностью, что имеет худший для нас результат и лучший для них (они выигрывают, а мы проигрываем).Таким образом, вы ставите себя на их место, стараетесь играть настолько хорошо, насколько можете, и предполагаете, что они это сделают.
Итак, если вы обнаружите, что у вас есть два возможных хода, один из которых дает им выбор: выиграть или проигратьВ любом случае, один из которых приводит к ничьей, вы предполагаете, что они пойдут на ход, который заставит их выиграть, если вы позволите им сделать это.Так что лучше пойти на ничью.

Теперь для более «алгоритмического» представления.

Представьте, что ваша сетка почти заполнена, за исключением двух возможных позиций.
Подумайте, что происходит, когда высыграйте первую:
Оппонент сыграет другую.Это единственный возможный ход, поэтому нам не нужно рассматривать другие ходы от них.Посмотрите на результат, свяжите результирующее значение (+ ∞, если выиграл, 0, если ничья, -∞, если проиграл: для крестики-нолики вы можете представить их как +1 0 и -1).
Теперь рассмотрим, что происходит, когда высыграйте второй:
(то же самое здесь, у противника есть только один ход, посмотрите на полученную позицию, оцените позицию).

Вам нужно выбрать между двумя ходами.Это наш ход, поэтому мы хотим получить лучший результат (это «максимум» в минимаксе).Выберите тот, у кого более высокий результат, в качестве нашего «лучшего» хода.Вот и все для примера «2 хода от конца».

Теперь представьте, что у вас осталось не 2, а 3 хода.
Принцип тот же, вы хотите присвоить значение каждому из 3 возможныхходы, так что вы можете выбрать лучший.
Итак, вы начинаете с рассмотрения одного из трех ходов.
Вы находитесь в ситуации выше, только с 2 возможными ходами, но это ход противника.Затем вы начинаете рассматривать один из возможных ходов для противника, как мы делали выше.Кроме того, вы смотрите на каждый из возможных ходов, и вы найдете значение результата для них обоих.Это ход противника, поэтому мы предполагаем, что они сыграют «лучший» ход для них, тот, который имеет худшую явку для нас, так что это тот, у которого меньшее значение (это «мин» в минимаксе).Игнорировать другой;Предположим, они все равно сыграют то, что вы нашли для них.Это то, что даст ваш ход, так что это значение, которое вы назначаете первому из трех ваших ходов.

Теперь вы рассматриваете каждый второй возможный ход.Вы даете им значение таким же образом.И из ваших трех ходов вы выбираете тот, который имеет максимальное значение.

Теперь рассмотрим, что происходит с 4 ходами.Для каждого из ваших 4-х ходов вы смотрите, что происходит с 3-мя ходами вашего противника, и для каждого из них вы предполагаете, что они выберут тот, который даст вам наихудший возможный результат из лучших из 2-х оставшихся для вас ходов.

Вы видите, куда это идет.Чтобы оценить ход n шагов от конца, вы смотрите на то, что может произойти для каждого из n возможных ходов, пытаясь дать им значение, чтобы вы могли выбрать лучший.В процессе вам нужно будет попытаться найти лучший ход для игрока, который играет с n-1: противником, и выбрать шаг с меньшим значением.В процессе оценки хода n-1 вы должны выбрать между возможными ходами n-2, которые будут нашими, и предположить, что мы будем играть так же хорошо, как можем на этом шаге.И т.д.

Вот почему этот алгоритм по своей сути рекурсивен.Независимо от n, на шаге n вы оцениваете все возможные шаги на n-1.Промыть и повторить.

Так как сегодняшние крестики-нолики достаточно мощные, чтобы вычислить все возможные результаты с самого начала игры, потому что их всего несколько сотен. Когда вы захотите реализовать его для более сложной игры, вам придется в какой-то момент прекратить вычисления, потому что это займет слишком много времени. Таким образом, для сложной игры вам также нужно будет написать код, который решит, продолжать ли искать все возможные последующие ходы или попытаться определить значение позиции сейчас и вернуться рано. Это означает, что вам также придется вычислять значение для позиции, которая не является окончательной - например, для шахмат вы должны учитывать, сколько материала у каждого оппонента на доске, непосредственные возможности проверки без мата, сколько плиток вы контролируете и все, что делает его не тривиальным.

3 голосов
/ 18 октября 2010

Ваша полная функция не работает должным образом, в результате чего игры объявляются связанными до того, как что-либо произойдет.Например, рассмотрим следующую настройку:

>> 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
...