Подтвердить что ты не робот

Minimax объяснил для идиота

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

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

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

Ответ 1

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

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

Ответ 2

Шаг 1: Создайте игровое дерево

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

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

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

Для простой игры, такой как Tic-Tac-Toe, сделайте счет 0, если вы проиграете, 50 галстуков, 100 побед.

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

Здесь используется min-max. Счет ранее незащищенного борта зависит от его детей и кто играет. Предположите, что вы и ваш оппонент всегда выбираете наилучшее движение в данном состоянии. Лучшим шагом для противника является ход, который дает вам худший результат. Точно так же ваш лучший ход - это шаг, который дает вам самый высокий балл. В случае поворота противника вы выбираете ребенка с минимальным счетом (что максимизирует его пользу). Если на ваш ход вы предполагаете, что сделаете максимально возможный ход, выберете максимум.

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

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

Попробуйте на листе бумаги, если начать с пустой доски слишком много, чтобы начать с какого-то продвинутого положения Tic-Tac-Toe.

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

Ответ 3

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

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

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

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

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

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

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

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

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

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

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