Введение в минимаксный алгоритм

Введение в минимаксный алгоритм

1. обзор

В этой статье мы собираемся обсудить алгоритм Minimax и его приложения в AI. Поскольку это алгоритм теории игр, мы реализуем на нем простую игру.

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

2. Вступление

Минимакс - алгоритм принятия решения,typically used in a turn-based, two player games. Цель алгоритма - найти оптимальный следующий ход.

В алгоритме один игрок называется максимизатором, а другой игрок - минимизатором. Если мы присваиваем оценочную оценку игровому полю, один игрок пытается выбрать игровое состояние с максимальным счетом, а другой выбирает состояние с минимальным счетом.

Другими словами,themaximizer works to get the highest score, while the minimizer tries get the lowest score by trying to counter moves.

Он основан на концепцииzero-sum game. In a zero-sum game, the total utility score is divided among the players. An increase in one player’s score results into the decrease in another player’s score. Итак, общий балл всегда равен нулю. Чтобы один игрок выиграл, другой должен проиграть. Примерами таких игр являются шахматы, покер, шашки, крестики-нолики.

Интересный факт: в 1997 году шахматный компьютер IBM Deep Blue (построенный с использованием Minimax) победил Гарри Каспарова (чемпиона мира по шахматам).

3. Минимаксный алгоритм

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

Для каждого шага мы можем смотреть вперед на столько ходов, сколько позволяет наша вычислительная мощность. Алгоритм предполагает, что противник играет оптимально.

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

Рассмотрим приведенное ниже дерево игр:

image

Maximizerstarts with the root node и выбирает ход с максимальным количеством очков. К сожалению, только листы имеют оценочные баллы, и, следовательно, алгоритм должен рекурсивно достигать узлов листьев. В данном игровом дереве сейчас очередь минимизатора наchoose a move from the leaf nodes, поэтому будут выбраны узлы с минимальными баллами (здесь узлы 3 и 4). Он продолжает выбирать лучшие узлы аналогичным образом, пока не достигнет корневого узла.

А теперь давайте формально определим шаги алгоритма:

  1. Построить полное дерево игры

  2. Оцените баллы на листьях, используя функцию оценки

  3. Резервные оценки от листьев к корню, учитывая тип игрока:

    • Для максимального игрока выберите ребенка с максимальным счетом

    • Для минимального игрока выберите ребенка с минимальным счетом

  4. В корневом узле выберите узел с максимальным значением и выполните соответствующее перемещение.

4. Реализация

А теперь давайте реализуем игру.

В игре у нас естьheap with n number of bones. Оба игрока должны в свою очередь получитьpick up 1,2 or 3 bones. Игрок, который не может взять кости, проигрывает. Каждый игрок играет оптимально. Учитывая значениеn, давайте напишем ИИ.

Чтобы определить правила игры, мы реализуем классGameOfBones:

class GameOfBones {
    static List getPossibleStates(int noOfBonesInHeap) {
        return IntStream.rangeClosed(1, 3).boxed()
          .map(i -> noOfBonesInHeap - i)
          .filter(newHeapCount -> newHeapCount >= 0)
          .collect(Collectors.toList());
    }
}

Кроме того, нам также нужна реализация для классовNode иTree:

public class Node {
    int noOfBones;
    boolean isMaxPlayer;
    int score;
    List children;
    // setters and getters
}
public class Tree {
    Node root;
    // setters and getters
}

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

public class MiniMax {
    Tree tree;

    public void constructTree(int noOfBones) {
        tree = new Tree();
        Node root = new Node(noOfBones, true);
        tree.setRoot(root);
        constructTree(root);
    }

    private void constructTree(Node parentNode) {
        List listofPossibleHeaps
          = GameOfBones.getPossibleStates(parentNode.getNoOfBones());
        boolean isChildMaxPlayer = !parentNode.isMaxPlayer();
        listofPossibleHeaps.forEach(n -> {
            Node newNode = new Node(n, isChildMaxPlayer);
            parentNode.addChild(newNode);
            if (newNode.getNoOfBones() > 0) {
                constructTree(newNode);
            }
        });
    }
}

Теперь мы реализуем методcheckWin, который будет моделировать игру, выбирая оптимальные ходы для обоих игроков. Это устанавливает счет к:

  • +1, если победит максимизатор

  • -1, если минимизатор выигрывает

checkWin вернет истину, если первый игрок (в нашем случае - максимайзер) выиграет:

public boolean checkWin() {
    Node root = tree.getRoot();
    checkWin(root);
    return root.getScore() == 1;
}

private void checkWin(Node node) {
    List children = node.getChildren();
    boolean isMaxPlayer = node.isMaxPlayer();
    children.forEach(child -> {
        if (child.getNoOfBones() == 0) {
            child.setScore(isMaxPlayer ? 1 : -1);
        } else {
            checkWin(child);
        }
    });
    Node bestChild = findBestChild(isMaxPlayer, children);
    node.setScore(bestChild.getScore());
}

Здесь методfindBestChild находит узел с максимальным счетом, если игрок максимизирует. В противном случае возвращается ребенок с минимальным баллом:

private Node findBestChild(boolean isMaxPlayer, List children) {
    Comparator byScoreComparator = Comparator.comparing(Node::getScore);
    return children.stream()
      .max(isMaxPlayer ? byScoreComparator : byScoreComparator.reversed())
      .orElseThrow(NoSuchElementException::new);
}

Наконец, давайте реализуем тестовый пример с некоторыми значениямиn (количество костей в куче):

@Test
public void givenMiniMax_whenCheckWin_thenComputeOptimal() {
    miniMax.constructTree(6);
    boolean result = miniMax.checkWin();

    assertTrue(result);

    miniMax.constructTree(8);
    result = miniMax.checkWin();

    assertFalse(result);
}

5. улучшение

Для большинства проблем невозможно построить целое дерево игр. In practice, we can develop a partial tree (construct the tree till a predefined number of levels only).

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

Даже если мы не построим полное дерево игр, вычисление ходов для игр с высоким коэффициентом ветвления может занять много времени.

Fortunately, there is an option to find the optimal move, without exploring every node дерева игры. Мы можем пропустить некоторые ветки, следуя некоторым правилам, и это не повлияет на конечный результат. This process is calledpruning. Alpha–beta pruning - распространенный вариант минимаксного алгоритма.

6. Заключение

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

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

Как всегда, полный код алгоритма можно найтиover on GitHub.