Перейти к основному содержимому

Введение в алгоритм Minimax с реализацией Java

· 5 мин. чтения

1. Обзор

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

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

2. Введение

Минимакс — это алгоритм принятия решений, обычно используемый в пошаговых играх для двух игроков . Цель алгоритма — найти оптимальный следующий ход.

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

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

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

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

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

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

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

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

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

./d76d208e52f99efdc2979ccde9847d55.png

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

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

  1. Построить полное игровое дерево
  2. Оцените баллы для листьев, используя функцию оценки
  3. Резервное копирование очков от листьев к корню с учетом типа игрока:
  • Для максимального игрока выберите ребенка с максимальным количеством очков.
  • Для минимального игрока выберите ребенка с минимальным счетом
  1. В корневом узле выберите узел с максимальным значением и выполните соответствующий ход.

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

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

В игре у нас есть куча с n количеством костей . Оба игрока должны поднять 1, 2 или 3 кости в свой ход. Игрок, который не может взять кости, проигрывает. Каждый игрок играет оптимально. Учитывая значение n , давайте напишем ИИ.

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

class GameOfBones {
static List<Integer> 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<Node> 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<Integer> 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 вернет true, если выиграет первый игрок (в нашем случае — максимизатор) :

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

private void checkWin(Node node) {
List<Node> 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<Node> children) {
Comparator<Node> 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. Улучшение

Для большинства задач невозможно построить все игровое дерево. На практике мы можем разработать частичное дерево (построить дерево только до заранее определенного количества уровней) .

Затем нам нужно будет реализовать функцию оценки, которая должна решить, насколько хорошее текущее состояние для игрока.

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

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

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

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

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

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