Para aplicar com sucesso min / max a um jogo de estratégia baseado em turnos, você precisa aplicar corretamente todas as técnicas de xadrez disponíveis ...
Função de avaliação
Até os motores de xadrez têm uma força muito ruim, se as funções de avaliação forem ruins. A versão mais simples de uma função de avaliação é: 1 = jogo vencido por brancos, -1 = jogo vencido por pretos, 0 = todos os outros casos; Mas, isso daria um desempenho muito ruim. O mesmo acontece com o seu jogo baseado em turnos! Se você deseja usar min / max (com poda alfa / beta e outras coisas) como no xadrez, também deve implementar uma função de avaliação razoável! Senão, você não pode comparar o desempenho desses algoritmos ao ser aplicado ao seu jogo de estratégia com o caso em que é aplicado ao xadrez.
O que as funções de avaliação dos mecanismos de xadrez fazem é avaliar coisas como:
- Quão bem está a posição de uma peça no quadro?
- Quantas vezes uma peça é atacada?
- Quantas vezes a peça está protegida?
- Quão bem cada peça pode "mover-se" livremente no quadro? (ou: quantos blocos ele "controla")
Essas partes da função de avaliação devem primeiro ser "traduzidas" para o seu jogo:
- Posição da peça: É, por exemplo, em uma colina, que está ampliando seu alcance de tiro?
- Atacado: Quanto cada peça está em perigo? (por exemplo, soma dos valores de ataque de unidades capazes de atacar uma unidade especial multiplicada por alguma probabilidade de ser atacada por ela; a probabilidade aumenta, se a unidade já estiver danificada; diminui se muitas outras unidades estiverem ao alcance da unidade atacante)
- Próprio Ataque: Quantas unidades podem ser atacadas por cada unidade?
- Proteção: Quantas peças existem ao lado (para ajudar)? Talvez uma unidade não possa atacar unidades a uma distância mínima e é preferível protegê-la por unidades com a possibilidade de atacar unidades próximas.
- Mobilidade: quão móvel é a sua unidade? (ele pode fugir?)
As diferentes classificações devem ser resumidas pela função de ponderação (fator_a * classificação_a + fator_b * ranting_b + ...) para todas as unidades ...
Nos jogos de estratégia, os recursos (ouro, madeira, ...) restantes devem ser levados em consideração.
Se a sua função de avaliação for boa o suficiente, na maioria dos casos você não precisará realmente pesquisar "profundamente" na árvore. Então, você provavelmente só precisa examinar mais de perto as 3 ou 10 opções mais promissoras. Veja o próximo capítulo ...
Movimentos possíveis em cada posição
A coisa mais problemática sobre o uso de min / max para jogos de estratégia é que você pode comandar várias unidades em um turno, enquanto no xadrez você só pode comandar uma unidade (exceto para roque, mas essa é uma combinação de movimentos claramente definida). Isso causa 5 ^ N movimentos possíveis para N unidades para cada "posição" (termo do xadrez), se você decidir entre "mover norte, sul, oeste, leste OU parar" para cada unidade. Você pode resolver isso dividindo o comando complexo em comandos de baixo nível: por exemplo, escolha a ação para a unidade A, entre em profundidade e decida pela unidade B .... decida pela unidade N ... e termine este turno. Mas, isso por si só não muda a complexidade! Você deve otimizar a ordem em que as ações são atribuídas às unidades (por exemplo, primeira unidade B, C, D e depois unidade A). Você pode registrar o impacto da decisão para cada unidade durante o último cálculo e depois classificar por importância. Dessa forma, a poda alfa-beta pode ser usada para eliminar qualquer combinação incorreta da árvore de pesquisa muito cedo. A prioridade mais alta deve sempre ser "não fazer mais nada e terminar o seu turno" (remoção de movimento nulo) em cada iteração. Dessa forma, você pode "pular" a atribuição da maioria das tarefas para a maioria das unidades e permitir que elas continuem o que fizeram antes. Dessa forma, a pesquisa será aprofundada rapidamente, basta dar uma olhada nas unidades "críticas" (por exemplo, as que estão realmente em combate no momento). Certifique-se de comandar apenas cada unidade uma vez ... Você também pode usar alguma aleatoriedade para garantir que as unidades "importantes" também recebam um comando de tempos em tempos. Especialmente, unidades que terminam algum trabalho (por exemplo,
Aprofundamento iterativo + cache / tabela de hash
Então, você pode "aprofundamento interativo" para aprofundar-se cada vez mais até que um prazo seja atingido. Portanto, você pesquisará mais profundamente se houver menos unidades e sempre terá algum "resultado" se parar de procurar uma solução melhor. O aprofundamento iterativo exigiria o uso de uma tabela de hash para armazenar em cache os resultados anteriores das pesquisas. Isso também permite reutilizar alguns dos resultados da última pesquisa de turnos (a ramificação da árvore de pesquisa que cobre os comandos que foram realmente executados na última rodada). Para implementar isso, você precisa de uma função de hash muito boa (consulte a "chave zobrist"), que pode ser atualizada iterativamente. Atualizar a chave de hash significa que você pode simplesmente pegar a chave de hash da antiga "posição" e pode simplesmente chutar a alteração na posição (por exemplo, retire a unidade na posição x e coloque-a na posição y). Dessa forma, o cálculo da chave de hash é rápido e você não precisa processar toda a situação das placas para calculá-la, apenas para verificar se o hash contém uma entrada anterior para esta posição. De certa forma, você deve garantir que não ocorram colisões de hash.
Comportamento não determinístico
O comportamento não determinístico é um problema para pesquisas mínimas / máximas. Isso significa que não há certeza se você atingirá um alvo atacado (por exemplo, a probabilidade é de 10%). Então você não pode apenas planejar isso acontecer. Nesse caso, você precisa modificar o algoritmo e colocar uma camada de "probabilidade" no meio. É um pouco como "as probabilidades mudam". Cada resultado independente deve ser considerado separadamente. A avaliação através dessa "camada" de profundidade deve ser amostrada (amostragem de Monte Carlo) e o resultado da avaliação detalhada deve ser ponderado pela probabilidade de ocorrência. Resultados diferentes da camada de probabilidade devem ser considerados como movimentos oponentes diferentes (mas, em vez de min / max, a "média" deve ser calculada). Obviamente, isso aumentará a complexidade da árvore de pesquisa.
Sumário
Ao aplicar todas essas técnicas (que são usadas pelos atuais mecanismos de xadrez) a um jogo determinístico, você certamente também poderá obter resultados razoáveis para um jogo. Para jogos não determinísticos, isso provavelmente será mais complicado, mas acho que ainda é administrável.
Um bom recurso para explicação dessas técnicas (para o xadrez) é http://chessprogramming.wikispaces.com/
Você pode até implementar algum tipo de aleatoriedade direcionada em pesquisas mínimas / máximas. Em vez de investigar deterministicamente os melhores resultados primeiro em cada iteração, você pode aleatoriamente fazer isso e deixar que sua ordem seja decidida por uma distribuição de probabilidade baseada nas avaliações atuais ...