Eu acho que eu poderia gerar todos os estados possíveis para um jogo, mas com quatro jogadores e 5 ações básicas (4 movimentos e local da bomba), ele fornece 5 ^ 4 estados no primeiro nível da árvore do jogo.
Corrigir! Você precisa pesquisar todas as ações de 5 ^ 4 (ou até 6 ^ 4, pois você pode caminhar em 4 direções, parar e "colocar uma bomba"?) Para cada ação do jogo. MAS, quando um jogador já decidiu se mudar, leva algum tempo até que a jogada seja executada (por exemplo, 10 ticks do jogo). Durante esse período, o número de possibilidades diminui.
Esse valor aumentará exponencialmente a cada próximo nível. Estou esquecendo de algo? Existem maneiras de implementá-lo ou devo usar um algoritmo totalmente diferente?
Você pode usar uma tabela de hash para calcular apenas o mesmo estado de jogo "subárvore" uma vez. Imagine que o jogador A anda para cima e para baixo, enquanto todos os outros jogadores "esperam", você acaba no mesmo estado do jogo. É o mesmo que para "esquerda-direita" ou "direita-esquerda". Também mover "para cima, então para a esquerda" e "para a esquerda e para cima" resulta no mesmo estado. Usando uma tabela de hash, você pode "reutilizar" a pontuação calculada para um estado de jogo que já foi avaliado. Isso reduz bastante a velocidade de crescimento. Matematicamente, reduz a base da sua função de crescimento exponencial. Para ter uma idéia de quanto isso reduz a complexidade, vamos analisar os movimentos possíveis para apenas um jogador em comparação com as posições alcançáveis no mapa (= diferentes estados do jogo) se o jogador puder apenas mover para cima / baixo / esquerda / direita / parada .
profundidade 1: 5 movimentos, 5 estados diferentes, 5 estados adicionais para esta recursão
profundidade 2: 25 movimentos, 13 estados diferentes, 8 estados adicionais para esta recursão
profundidade 3: 6125 movimentos, 25 estados diferentes, 12 estados adicionais para esta recursão
Para visualizar isso, responda a si mesmo: quais campos no mapa podem ser alcançados com um movimento, dois movimentos, três movimentos. A resposta é: Todos os campos com uma distância máxima = 1, 2 ou 3 da posição inicial.
Ao usar uma HashTable, você só precisa avaliar cada estado do jogo acessível (no nosso exemplo 25, na profundidade 3) uma vez. Considerando que, sem uma HashTable, é necessário avaliá-las várias vezes, o que significaria 6125 avaliações em vez de 25 no nível de profundidade 3. O melhor: depois de calcular uma entrada da HashTable, você pode reutilizá-la em etapas posteriores ...
Você também pode usar subárvores de "corte" de aprofundamento incremental e poda alfa-beta que não valem a pena pesquisar em mais profundidade. No xadrez, isso reduz o número de nós pesquisados para cerca de 1%. Uma breve introdução à poda alfa-beta pode ser encontrada em vídeo aqui: http://www.teachingtree.co/cs/watch?concept_name=Alpha-beta+Pruning
Um bom começo para mais estudos é http://chessprogramming.wikispaces.com/Search . A página está relacionada ao xadrez, mas os algoritmos de pesquisa e otimização são os mesmos.
Outro (mas complexo) algoritmo de IA - que seria mais adequado ao jogo - é o "Aprendizado de diferença temporal".
Saudações
Stefan
PS: Se você reduzir o número possível de estados de jogo (por exemplo, tamanho muito pequeno do mapa, apenas uma bomba por jogador, nada mais), há uma chance de pré-calcular uma avaliação para todos os estados de jogo.
--editar--
Você também pode usar os resultados calculados offline dos cálculos de minimax para treinar uma rede neuronal. Ou você pode usá-los para avaliar / comparar estratégias implementadas manualmente. Por exemplo, você pode implementar algumas das "personalidades" sugeridas e algumas heurísticas que detectam, em quais situações a estratégia é boa. Portanto, você deve "classificar" situações (por exemplo, estados do jogo). Isso também pode ser tratado por uma rede neuronal: Treine uma rede neuronal para prever qual das estratégias codificadas manualmente está desempenhando melhor na situação atual e execute-a. Isso deve produzir extremamente boas decisões em tempo real para um jogo real. Muito melhor do que uma pesquisa com limite de profundidade baixo que pode ser alcançada de outra forma, pois não importa quanto tempo demoram os cálculos offline (eles são antes do jogo).
- editar # 2 -
Se você apenas recalcular seus melhores movimentos a cada 1 segundo, também poderá tentar fazer um planejamento de nível mais alto. O que quero dizer com isso? Você sabe quantos movimentos você pode fazer em 1 segundo. Assim, você pode fazer uma lista de posições alcançáveis (por exemplo, se isso for 3 movimentos em 1 segundo, você terá 25 posições alcançáveis). Então você pode planejar como: vá para a "posição xe coloque uma bomba". Como alguns outros sugeriram, você pode criar um mapa de "perigo", que é usado para o algoritmo de roteamento (como ir para a posição x? Qual caminho deve ser preferido [existem algumas variações possíveis na maioria dos casos]). Isso consome menos memória em comparação com uma enorme HashTable, mas produz resultados menos ideais. Porém, como usa menos memória, pode ser mais rápido devido aos efeitos de armazenamento em cache (melhor uso dos caches de memória L1 / L2).
ADICIONALMENTE: Você pode fazer pesquisas prévias que contêm apenas movimentos para um jogador cada para classificar as variações que resultam em perda. Portanto, tire todos os outros jogadores do jogo ... Armazene quais combinações cada jogador pode escolher sem perder. Se houver apenas movimentos perdidos, procure as combinações de movimentos em que o jogador permanece vivo por mais tempo. Para armazenar / processar esse tipo de estrutura de árvore, você deve usar uma matriz com indicadores de índice como este:
class Gamestate {
int value;
int bestmove;
int moves[5];
};
#define MAX 1000000
Gamestate[MAX] tree;
int rootindex = 0;
int nextfree = 1;
Cada estado possui um "valor" de avaliação e vincula-se aos próximos Gamestates ao se mover (0 = parar, 1 = para cima, 2 = para a direita, 3 = para baixo, 4 = para a esquerda) armazenando o índice da matriz dentro de "árvore" em movimentos [0 ] para mover [4]. Para construir sua árvore recursivamente, isso pode ser assim:
const int dx[5] = { 0, 0, 1, 0, -1 };
const int dy[5] = { 0, -1, 0, 1, 0 };
int search(int x, int y, int current_state, int depth_left) {
// TODO: simulate bombs here...
if (died) return RESULT_DEAD;
if (depth_left == 0) {
return estimate_result();
}
int bestresult = RESULT_DEAD;
for(int m=0; m<5; ++m) {
int nx = x + dx[m];
int ny = y + dy[m];
if (m == 0 || is_map_free(nx,ny)) {
int newstateindex = nextfree;
tree[current_state].move[m] = newstateindex ;
++nextfree;
if (newstateindex >= MAX) {
// ERROR-MESSAGE!!!
}
do_move(m, &undodata);
int result = search(nx, ny, newstateindex, depth_left-1);
undo_move(undodata);
if (result == RESULT_DEAD) {
tree[current_state].move[m] = -1; // cut subtree...
}
if (result > bestresult) {
bestresult = result;
tree[current_state].bestmove = m;
}
}
}
return bestresult;
}
Esse tipo de estrutura de árvore é muito mais rápido, pois a alocação dinâmica de memória é realmente muito lenta! Mas, armazenar a árvore de pesquisa também é bastante lento ... Isso é mais uma inspiração.