Example #1
0
        /// <summary>
        /// Executa a pesquisa alfa beta.
        /// </summary>
        /// <remarks>
        /// Essa função deve ser a mais executada para o motor de xadrez. A maior parte da diversão
        /// acontece aqui, o que significa que há muitas técnicas que podem ser tentadas.
        /// Vamos comentar as técnicas diretamente no código abaixo, mas a idéia geral é passar mais
        /// tempo procurando movimentos promissores e passar menos tempo buscando possíveis movimentos
        /// ruins.
        /// A busca às vezes descarta alguns movimentos, e às vezes estende outros. Por exemplo, estender
        /// a busca por um movimento de xeque é geralmente bom. Evitar pesquisar movimentos no final da
        /// lista de movimentos também é geralmente bom.
        /// Há sempre algumas idéias que podem ser tentadas, quando você no código de outros motores você
        /// pode obter alguma inspiração para suas próprias idéias e tentar no seu programa. A maioria das
        /// ideias em outros motores tem que ser adaptada ao seu programa, às vezes eles simplesmente não
        /// funcionam, porque seu programa possui uma estrutura diferente.
        /// E um ponto importante, que é considerado cortesia, quando você menciona de onde você teve a
        /// idéia / inspiração.
        /// </remarks>
        /// <param name="alfa">Limite inferior da pesquisa.</param>
        /// <param name="beta">Limite superior da pesquisa.</param>
        /// <param name="nivel">Distância da posição inicial (conhecido como ply). Aumentada a cada chamada.</param>
        /// <param name="profundidade">Profundidade da pesquisa (depth), número de movimentos para olhar a frente. Diminui a cada chamada.</param>
        /// <param name="variacao_principal">Lista dos melhores movimentos localizados durante a pesquisa.</param>
        /// <returns>Melhor valor encontrado para a posição.</returns>
        public int AlfaBeta(int alfa, int beta, int nivel, int profundidade, List <Movimento> variacao_principal)
        {
            Debug.Assert(alfa >= Defs.VALOR_MINIMO);
            Debug.Assert(beta <= Defs.VALOR_MAXIMO);
            Debug.Assert(beta > alfa);
            Debug.Assert(nivel >= 0 && nivel <= Defs.NIVEL_MAXIMO);
            Debug.Assert(profundidade >= 0 && profundidade <= Defs.PROFUNDIDADE_MAXIMA);
            Debug.Assert(variacao_principal != null);

            // Final da pesquisa por causa do tempo ou profundidade ?
            VerificaTerminoProcura();
            if (EncerraProcura)
            {
                return(0);
            }

            // Programação defensiva para evitar erros de índice fora da faixa da tabela.
            if (nivel > Defs.NIVEL_MAXIMO - 1)
            {
                return(Avaliacao.ObtemPontuacao());
            }

            // Chegamos a uma posição que é empate por causa das regras do xadrez.
            if (nivel > 0 && Tabuleiro.EmpatePorRegra50())
            {
                return(0);
            }
            if (nivel > 0 && Tabuleiro.EmpatePorRepeticao())
            {
                return(0);
            }

            // Ao chegar ao final da pesquisa, vai para a pesquisa quiescente para obter o resultado final.
            if (profundidade <= 0)
            {
                return(Quiescente(alfa, beta, nivel, variacao_principal));
            }

            // Contador de posição. Apenas para fins informativos, para que você veja o quão rápido é o seu motor.
            ContadorPosicoes++;

            // Prepara lista the movimentos encontrados.
            if (nivel > 0)
            {
                variacao_principal.Clear();
            }


            // Accesso a informações da tabela de transposição (Transposition Table Probe).
            // Nota: alguns programas usam Hash Table ao inves the Transposition Table.
            // Se esta posição foi visitada antes, a pontuação pode estar na tabela de transposição,
            // podemos reutilizá-la e retornar daqui. Esta é uma grande economia.
            // Esta informação será salva durante a pesquisa, quando tivermos a informação sobre a pontuação.
            // É importante usar o valor somente se a profundidade na tabela for maior ou igual à profundidade atual.
            // Se não podemos usar a pontuação, podemos usar o melhor movimento, que pode causar um corte beta e
            // também economizar tempo. Lembre-se de que isso ajuda na ordenação do movimentos.
            Movimento movimento_transposicao = null;
            var       registro = Transposicao.Recupera(Tabuleiro.Chave, profundidade);

            if (registro != null)
            {
                if (registro.PodeUsarValor(alfa, beta))
                {
                    return(Transposicao.AjustaValorParaProcura(registro.Valor, nivel));
                }
                movimento_transposicao = registro.Movimento;
            }

            // Preparação de informações para a pesquisa.
            var cor_jogar_esta_em_cheque = Tabuleiro.CorJogarEstaEmCheque();
            var valor_avaliacao          = Avaliacao.ObtemPontuacao();
            var nova_variacao_principal  = new List <Movimento>();

            // "Passar a navalha" (Razoring).
            // Talvez a tradução de razoring não seja boa, mas aqui vamos tentar remover posições
            // que não são muito promissoras, com uma busca reduzida. Vamos tentar passar a navalha nessas posições !
            // O valor de avaliação mais um valor estimado já é menor do que o alfa, então, se a busca reduzida
            // confirmar que não há uma boa captura, podemos descartar essa posição e ignorar a pesquisa.
            if (profundidade <= 3 && !cor_jogar_esta_em_cheque && valor_avaliacao + 150 * profundidade < alfa)
            {
                int alfa_reduzido = alfa - 150 * profundidade;
                int valor         = Quiescente(alfa_reduzido, alfa_reduzido + 1, nivel, nova_variacao_principal);
                if (EncerraProcura)
                {
                    return(0);
                }
                if (valor <= alfa_reduzido)
                {
                    return(valor);
                }
            }

            // Este é o famoso movimento nulo (movimento nulo). Praticamente todos os motores de xadrez o implementam.
            // A idéia é que você está em uma posição que é tão boa (valor maior do que beta) que mesmo se você passar
            // o direito de mover para o outro jogador, dando-lhe dois movimentos em sequência, ele não será capaz de recuperar.
            // Então, você pode descartar essa pesquisa. É um bom ganho.
            // Existem algumas boas práticas para movimentos nulos:
            // - Não faça dois movimentos nulos em seqüência.
            // - Não pode estar no xeque, pois pode gerar posições ilegais.
            // - É bom que você tenha algumas peças.
            // - Também não é recomendado para pesquisa de variação principal.
            // O movimento nulo deve ser confirmado com uma busca reduzida.
            // O valor obtido pode ser salvo na tabela de transposição.
            if (profundidade > 3 && !cor_jogar_esta_em_cheque && alfa == beta - 1 && valor_avaliacao >= beta)
            {
                if (!Tabuleiro.MovimentoAnteriorFoiNulo() && Tabuleiro.TemPecas(Tabuleiro.CorJogar))
                {
                    Tabuleiro.FazMovimentoNulo();
                    int valor = -AlfaBeta(-beta, -beta + 1, nivel + 1, profundidade - 3, nova_variacao_principal);
                    Tabuleiro.DesfazMovimentoNulo();
                    if (EncerraProcura)
                    {
                        return(0);
                    }
                    if (valor >= beta)
                    {
                        if (valor > Defs.AVALIACAO_MAXIMA)
                        {
                            valor = beta;                                // // Valor de mate, não muito confiável neste caso.
                        }
                        Transposicao.Salva(Tabuleiro.Chave, profundidade, valor, nivel, Transposicao.Tipo.INFERIOR, null);
                        return(valor);
                    }
                }
            }

            // Prepara a próxima profundidade. Se estiver em xeque, estendemos a pesquisa.
            // Esta é uma extensão de xeque simples, pode ser implementada em diferentes formas.
            int nova_profundidade = profundidade - 1;

            if (cor_jogar_esta_em_cheque)
            {
                nova_profundidade += 1;
            }

            // Mais itens de controle de pesquisa.
            int       melhor_valor        = Defs.VALOR_MINIMO;
            int       contador_movimentos = 0;
            Movimento melhor_movimento    = null;

            // Gera e classifica a lista de movimentos. Usa o movimento da tabela de transposição, se disponível.
            var lista = Tabuleiro.GeraMovimentos();

            lista = Ordenacao.Orderna(Tabuleiro.CorJogar, lista, movimento_transposicao);

            // Loop dos movimentos.
            foreach (var movimento in lista)
            {
                Tabuleiro.FazMovimento(movimento);
                if (!Tabuleiro.MovimentoFeitoLegal())
                {
                    Tabuleiro.DesfazMovimento();
                    continue;
                }
                Debug.Assert(Tabuleiro.Chave == Zobrist.ObtemChave(Tabuleiro));

                contador_movimentos += 1;

                // Início da pesquisa recursiva. O objectivo é obter o valor para este movimento nesta profundidade.
                int valor_procura = 0;
                if (melhor_valor == Defs.VALOR_MINIMO)
                {
                    // O primeiro movimento será pesquisado com o tamanho da janela inteira, ou seja, os valores alfa / beta invertidos.
                    valor_procura = -AlfaBeta(-beta, -alfa, nivel + 1, nova_profundidade, nova_variacao_principal);
                    // Os movimentos seguintes são pesquisados com uma pesquisa de janela zero. Em vez de usar -beta, -alfa para a
                    // janela de pesquisa, usaremos -alfa-1, -alfa. Isso deve descartar mais movimentos porque os valores
                    // alfa / beta da próxima iteração serão próximos. Mas antes podemos aplicar algumas técnicas para reduzir a
                    // quantidade de movimentos pesquisados. Veja abaixo na parte do "else".
                }
                else
                {
                    // Poda de futilidade - Futility Pruning.
                    // Se o valor da avaliação mais um valor estimado for inferior ao valor alfa, podemos ignorar esse movimento.
                    // Mas temos que considerar algumas restrições como abaixo. Existem muitas maneiras diferentes de implementar
                    // esta técnica, mais uma vez você pode ajustar de acordo com sua preferência e se ela funcionar para o seu motor.
                    if (!cor_jogar_esta_em_cheque && nova_profundidade == 1 && !movimento.Tatico() && alfa == beta - 1 && valor_avaliacao + 100 < alfa)
                    {
                        Tabuleiro.DesfazMovimento();
                        continue;
                    }

                    // Redução de movimento tardios - Late move reduction ou LMR.
                    // Para movimentos que estão mais próximos do fim da lista, podemos reduzir a profundidade em que são pesquisados.
                    // Esta técnica também pode ser adaptada de muitas formas diferentes.
                    int reducao = 0;
                    if (!cor_jogar_esta_em_cheque && nova_profundidade > 1 && contador_movimentos > 4 && !movimento.Tatico() && alfa == beta - 1 && valor_avaliacao < alfa)
                    {
                        reducao = 1;
                    }

                    // Outras técnicas de conhecimento podem ser aplicadas aqui. E talvez você possa criar uma nova técnica!

                    // Executa a pesquisa de janela zero.
                    valor_procura = -AlfaBeta(-alfa - 1, -alfa, nivel + 1, nova_profundidade - reducao, nova_variacao_principal);

                    // Quando a busca foi reduzida e o valor retornado é maior do que o alfa, significa que podemos ter um bom
                    // movimento, e temos que pesquisar sem a redução para confirmar. Esperava-se que não tivesse um bom movimento
                    // neste caso.
                    if (!EncerraProcura && valor_procura > alfa && reducao != 0)
                    {
                        valor_procura = -AlfaBeta(-alfa - 1, -alfa, nivel + 1, nova_profundidade, nova_variacao_principal);
                    }

                    // Este é outro caso de re-pesquisa, depois de pesquisar com janela zero. Esperava-se não encontrar bons movimentos
                    // neste caso. Se o valor retornado for maior que o alfa, significa que devemos pesquisar novamente com a janela
                    // completa, para confirmar o movimento bom.
                    if (!EncerraProcura && valor_procura > alfa && valor_procura < beta)
                    {
                        valor_procura = -AlfaBeta(-beta, -alfa, nivel + 1, nova_profundidade, nova_variacao_principal);
                    }
                    // Nota: Pode parecer que o motor pode pesquisar a mesma posição duas vezes, mas se você seguir a lógica acima,
                    // será uma ou outra.
                }

                Tabuleiro.DesfazMovimento();
                if (EncerraProcura)
                {
                    return(0);
                }
                Debug.Assert(Tabuleiro.Chave == Zobrist.ObtemChave(Tabuleiro));

                // Agora que temos o valor para este movimento nesta posição, podemos comparar a alfa e beta tomar decisões.
                // Se o valor for maior que o beta, temos um corte beta. Isso significa que não precisamos pesquisar o resto dos
                // movimentos. O objetivo é obter muitos cortes beta, especialmente no primeiro movimento.
                // É importante atualizar o histórico de movimentos e a tabela de transposição, então quando pesquisamos essa
                // posição novamente, possivelmente também podemos ter outro corte beta.
                if (valor_procura >= beta)
                {
                    Ordenacao.AtualizaHistoria(Tabuleiro.CorJogar, movimento, profundidade);
                    Transposicao.Salva(Tabuleiro.Chave, profundidade, valor_procura, nivel, Transposicao.Tipo.INFERIOR, movimento);
                    return(valor_procura);
                }

                // Se não tivermos um corte beta, temos que gravar o melhor resultado até agora, e aumentar o valor da alfa. Isso significa
                // que temos uma boa jogada para o cargo, e podemos atualizar a variação principal.
                if (valor_procura > melhor_valor)
                {
                    melhor_valor = valor_procura;
                    if (valor_procura > alfa)
                    {
                        alfa             = valor_procura;
                        melhor_movimento = movimento;
                        AtualizaVariacaoPrincipal(variacao_principal, nova_variacao_principal, movimento);
                        if (nivel == 0)
                        {
                            ImprimeVariacaoPrincipal(valor_procura, profundidade, variacao_principal);
                        }
                    }
                }
            }

            // Acabamos de pesquisar todos movimentos. Precisamos fazer mais alguns passos.

            // Se todos os movimentos forem ilegais, significa que não podemos mover nessa posição.
            // Se o nosso rei estiver em xeque, então é xeque mate, esta posição está perdida, e nós retornamos uma pontuação de xeque.
            // Se o nosso rei não estiver em xeque, então é empate. Não podemos fazer um movimento sem colocar o rei no xeque.
            if (melhor_valor == Defs.VALOR_MINIMO)
            {
                return(cor_jogar_esta_em_cheque ? -Defs.VALOR_MATE + nivel : 0);
            }

            // Aqui vamos atualizar o histórico de movimentos e a tabela de transposição de acordo com o escore.
            // Podemos encontrar uma bom movimento, ou nenhum movimento foi capaz de melhorar alfa.
            if (melhor_movimento != null)
            {
                Ordenacao.AtualizaHistoria(Tabuleiro.CorJogar, melhor_movimento, profundidade);
                Transposicao.Salva(Tabuleiro.Chave, profundidade, melhor_valor, nivel, Transposicao.Tipo.EXATO, melhor_movimento);
            }
            else
            {
                Transposicao.Salva(Tabuleiro.Chave, profundidade, melhor_valor, nivel, Transposicao.Tipo.SUPERIOR, null);
            }

            // Retorna o valor da posição.
            return(melhor_valor);
        }