Introdução ao algoritmo Minimax
1. Visão geral
Neste artigo, vamos discutir o algoritmo Minimax e suas aplicações em IA. Como é um algoritmo de teoria dos jogos, vamos implementar um jogo simples usando-o.
Também discutiremos as vantagens de usar o algoritmo e ver como ele pode ser melhorado.
2. Introdução
Minimax é um algoritmo de tomada de decisão,typically used in a turn-based, two player games. O objetivo do algoritmo é encontrar o próximo passo ideal.
No algoritmo, um jogador é chamado de maximizador e o outro é um minimizador. Se atribuirmos uma pontuação de avaliação ao tabuleiro de jogo, um jogador tenta escolher um estado de jogo com a pontuação máxima, enquanto o outro escolhe um estado com a pontuação mínima.
Em outras palavras,themaximizer works to get the highest score, while the minimizer tries get the lowest score by trying to counter moves.
É baseado no conceitozero-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. Portanto, a pontuação total é sempre zero. Para um jogador vencer, o outro precisa perder. Exemplos de tais jogos são xadrez, pôquer, damas, jogo da velha.
Um fato interessante - em 1997, o computador de xadrez da IBM Deep Blue (construído com Minimax) derrotou Garry Kasparov (o campeão mundial de xadrez).
3. Algoritmo Minimax
Nosso objetivo é encontrar a melhor jogada para o jogador. Para isso, basta escolher o nó com a melhor pontuação de avaliação. Para tornar o processo mais inteligente, também podemos olhar em frente e avaliar os movimentos do oponente em potencial.
Para cada movimento, podemos olhar para o maior número de movimentos que nosso poder de computação permitir. O algoritmo assume que o oponente está jogando da melhor maneira.
Tecnicamente, começamos com o nó raiz e escolhemos o melhor nó possível. Avaliamos nós com base em suas pontuações de avaliação. No nosso caso, a função de avaliação pode atribuir pontuações apenas aos nós de resultados (folhas). Portanto, alcançamos recursivamente folhas com pontuações e propagamos as pontuações para trás.
Considere a árvore do jogo abaixo:
Maximizerstarts with the root node e escolhe o lance com a pontuação máxima. Infelizmente, apenas as folhas têm pontuações de avaliação com elas e, portanto, o algoritmo deve atingir os nós das folhas recursivamente. Na árvore do jogo dada, atualmente é a vez do minimizador parachoose a move from the leaf nodes, então os nós com pontuações mínimas (aqui, nós 3 e 4) serão selecionados. Ele continua escolhendo os melhores nós da mesma forma, até atingir o nó raiz.
Agora, vamos definir formalmente as etapas do algoritmo:
-
Construa a árvore completa do jogo
-
Avalie pontuações para folhas usando a função de avaliação
-
Pontuações de backup das folhas para a raiz, considerando o tipo de jogador:
-
Para jogador máximo, selecione a criança com a pontuação máxima
-
Para jogador min, selecione a criança com a pontuação mínima
-
-
No nó raiz, escolha o nó com valor máximo e execute a movimentação correspondente
4. Implementação
Agora, vamos implementar um jogo.
No jogo, temos umheap with n number of bones. Ambos os jogadores têm depick up 1,2 or 3 bones na sua vez. Um jogador que não pode pegar nenhum osso perde o jogo. Cada jogador joga da melhor maneira. Dado o valor den, vamos escrever um AI.
Para definir as regras do jogo, implementaremos a classeGameOfBones:
class GameOfBones {
static List getPossibleStates(int noOfBonesInHeap) {
return IntStream.rangeClosed(1, 3).boxed()
.map(i -> noOfBonesInHeap - i)
.filter(newHeapCount -> newHeapCount >= 0)
.collect(Collectors.toList());
}
}
Além disso, também precisamos da implementação para as classesNodeeTree:
public class Node {
int noOfBones;
boolean isMaxPlayer;
int score;
List children;
// setters and getters
}
public class Tree {
Node root;
// setters and getters
}
Agora vamos implementar o algoritmo. Requer uma árvore de jogo para olhar em frente e encontrar a melhor jogada. Vamos implementar isso:
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);
}
});
}
}
Agora, implementaremos o métodocheckWin que simulará uma jogada, selecionando os movimentos ideais para ambos os jogadores. Ele define a pontuação para:
-
+1, se o maximizador vencer
-
-1, se o minimizador vencer
OcheckWin retornará verdadeiro se o primeiro jogador (no nosso caso - maximizador) vencer:
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());
}
Aqui, o métodofindBestChild encontra o nó com a pontuação máxima se um jogador for um maximizador. Caso contrário, ele retorna a criança com a pontuação mínima:
private Node findBestChild(boolean isMaxPlayer, List children) {
Comparator byScoreComparator = Comparator.comparing(Node::getScore);
return children.stream()
.max(isMaxPlayer ? byScoreComparator : byScoreComparator.reversed())
.orElseThrow(NoSuchElementException::new);
}
Finalmente, vamos implementar um caso de teste com alguns valores den (o número de ossos em um heap):
@Test
public void givenMiniMax_whenCheckWin_thenComputeOptimal() {
miniMax.constructTree(6);
boolean result = miniMax.checkWin();
assertTrue(result);
miniMax.constructTree(8);
result = miniMax.checkWin();
assertFalse(result);
}
5. Melhoria
Para a maioria dos problemas, não é possível construir uma árvore de jogo inteira. In practice, we can develop a partial tree (construct the tree till a predefined number of levels only).
Então, teremos que implementar umevaluation function, que deve ser capaz de decidir o quão bom está o estado atual, para o jogador.
Mesmo se não construirmos árvores de jogo completas, pode ser demorado calcular movimentos para jogos com alto fator de ramificação.
Fortunately, there is an option to find the optimal move, without exploring every node da árvore do jogo. Podemos pular alguns ramos seguindo algumas regras, e isso não afetará o resultado final. This process is calledpruning. Alpha–beta pruning é uma variante predominante do algoritmo minimax.
6. Conclusão
O algoritmo Minimax é um dos algoritmos mais populares para jogos de tabuleiro de computador. É amplamente aplicado em jogos baseados em turnos. Pode ser uma boa escolha quando os jogadores tiverem informações completas sobre o jogo.
Pode não ser a melhor opção para os jogos com fator de ramificação excepcionalmente alto (por exemplo, jogo de GO). No entanto, dada uma implementação adequada, pode ser uma IA bastante inteligente.
Como sempre, o código completo para o algoritmo pode ser encontradoover on GitHub.