JOGO DE XADREZ EM DELPHI: CAPÍTULO 7 - DETALHES FINAIS DO TABULEIRO E INTELIGÊNCIA ARTIFICIAL
Adicionado em 16/12/2023
Vamos agora abordar a parte final do nosso tutorial para criar um tabuleiro de xadrez funcional em Delphi. Nesta etapa, além de finalizar o design do nosso tabuleiro, vamos habilitar a funcionalidade de desfazer e refazer movimentos, permitir a edição do tabuleiro e exibir os movimentos em uma lista de forma precisa. Além disso, daremos um passo adiante, não apenas criando um tabuleiro totalmente funcional, mas também mostrando como podemos usá-lo como uma interface gráfica para interagir com poderosas engines de xadrez, como o Stockfish.
As engines de xadrez, também conhecidas como motores de xadrez, são inteligências artificiais especializadas que não possuem uma interface gráfica própria, mas são capazes de realizar cálculos avançados para determinar o melhor movimento em uma partida de xadrez. Neste tutorial, usaremos nosso tabuleiro como uma interface gráfica para uma das engines mais poderosas disponíveis, o Stockfish.
Para aprimorar a qualidade do nosso tabuleiro, vamos começar adicionando coordenadas que identifiquem cada uma das casas no tabuleiro de xadrez. As coordenadas em tabuleiros de xadrez são geralmente representadas por números para as linhas e letras para as colunas. Dado que o tabuleiro possui um tamanho padrão de 8x8 casas, as linhas são numeradas de 1 a 8 e as colunas são identificadas pelas letras de A até H.
Para exibir essas coordenadas nas laterais do nosso tabuleiro, siga os passos abaixo:
O comando Canvas faz parte da biblioteca que permite a criação de desenhos dentro do seu formulário (Form) ou de qualquer componente. Neste caso, estamos usando as coordenadas do nosso tabuleiro, que é do tipo TStringGrid, para criar os pontos onde os desenhos das coordenadas externas do tabuleiro serão adicionados.
Para criar esse efeito, escolhemos a cor preta como fundo (Canvas.Brush.Color := clBlack) e o texto é exibido em branco (Canvas.Font.Color := clWhite). Para alinhar o texto de forma correta com as casas do tabuleiro, utilizamos um simples cálculo matemático juntamente com um loop FOR.
Após adicionar esse código e executar o seu programa, o seu tabuleiro deve ficar assim:
Agora, vamos adicionar uma lista de movimentos que foram realizados durante a partida de Xadrez. Para fazer isso, vamos incluir um componente TListBox em nosso formulário (Form). Em seguida, no evento TabuleiroMouseUp, vamos adicionar o seguinte código:
Agora, vá até a parte superior do seu código, e declare a função AdListaMov:
Pressione Ctrl + Shift + C para declarar o código e adicione o seguinte código:
Neste código, começamos com o contador, que depende diretamente do movimento atual no índice de movimentos. Como queremos fazer uma contagem a cada vez que as peças brancas e pretas realizam seus movimentos, usamos a função matemática Ceil para arredondar o valor da divisão, garantindo que o contador varie a cada dois movimentos. A função Ceil é utilizada para arredondar um número para o inteiro mais próximo, de modo que, se o resultado da divisão não for um número inteiro, ele será arredondado para cima. Para usar a função Ceil, é necessário declarar a unidade (uses) Math na parte superior de sua unit, pois a função faz parte da biblioteca Math do Delphi.
Verificamos, por meio da lista de movimentos recebida pela variável de comunicação da função Mov (TMovimento), qual tipo de movimento foi realizado. Se o movimento envolver captura, promoção, roque, cheque ou cheque mate, adicionamos identificadores diferentes à nossa lista de movimentos. Caso contrário, simplesmente adicionamos o movimento, exibindo qual peça se moveu, a casa de origem e o destino da peça. É importante notar que utilizamos a nossa constante Tabu, que contém os nomes baseados nas coordenadas do tabuleiro, para representar as casas.
Ao executar o seu programa e realizar alguns movimentos no tabuleiro, a sua lista deve se parecer com o exemplo abaixo:
Adicione três TButton ao seu Form e mude o caption deles no Object Inspector para Novo Jogo, Avançar e Voltar.
Para adicionar os comando de um Novo Jogo temos que reinicializar as variáveis tais como havíamos utilizado no evento OnCreate. Além disso, temos que apagar os movimentos contidos no ListBox. Para isso, dê dois cliques no botão Novo Jogo e adicione o seguinte código:
Aqui, realizamos a reinicialização das variáveis, da mesma forma que fizemos no evento OnCreate do Formulário. Além disso, definimos o valor de IndiceMovimentoAtual como -1, que é o valor inicial no início do jogo, e ajustamos o tamanho da matriz de movimentos, "HistoricoMovimentos," para 0, efetuando a limpeza de todos os movimentos previamente armazenados.
Agora, clique duas vezes no botão "Voltar" e adicione o código a seguir:
A funcionalidade "Voltar" do nosso jogo é bastante simples e executa o processo inverso dentro do nosso histórico de movimentos. Em vez de incrementar o valor em uma unidade a cada vez, agora decrementamos o valor em uma unidade sempre que o botão é pressionado. Também utilizamos os identificadores de movimento, como "EnPassant", "promoção do peão" e "roque do rei," para executar corretamente o movimento de retrocesso. Esses identificadores são necessários porque, ao contrário dos outros movimentos, esses movimentos envolvem duas peças e movimentações personalizadas.
Por fim, clique duas vezes no botão "Avançar" e adicione o seguinte código:
Nesta função, agora voltamos a incrementar de 1 em 1 o valor do índice de movimentos e novamente temos o cuidado de levar em conta os identificadores de movimentos especiais para que eles sejam refeitos de forma correta.
Muitas vezes, não desejamos que o jogo comece desde o início, especialmente quando estamos estudando posições específicas. Nesse caso, podemos permitir que os jogadores configurem a posição inicial das peças conforme desejarem.
O exemplo que apresentaremos é bastante simples, permitindo que os jogadores configurem a posição das peças arbitrariamente, sem se preocupar com o estado do roque ou a contagem correta de peças no tabuleiro. Se você desejar criar um editor mais avançado, poderá adicionar opções que indiquem ao tabuleiro se o roque já foi realizado e que impeçam a adição de posições absurdas, como ter dois reis da mesma cor no tabuleiro (no nosso exemplo, isso é permitido, mas você pode restringir conforme necessário).
Para implementar esse recurso, adicione um TButton, um TEdit e um TCheckBox ao seu Form. No Object Inspector, altere o caption do seu botão para "Modo Edição: Desligado". Em seguida, clique duas vezes no botão e adicione o seguinte código:
Observe que, no código do botão, adicionamos comandos para alterar o próprio nome do botão quando ele é clicado, indicando se o modo de edição está ativo ou não. Além disso, declaramos uma variável global chamada ModoEdicao, que servirá como um sinalizador em outras funções. Utilizaremos o CheckBox para determinar se é a vez das peças brancas ou pretas jogarem. Também alteramos a propriedade Visible do CheckBox, tornando-o visível apenas no modo de edição, evitando assim problemas durante o jogo.
Retorne ao seu Form, clique no CheckBox1 e altere seu caption para "Vez das Brancas". No Object Inspector, defina a propriedade Visible do CheckBox1 como False e a propriedade Checked como True. Isso fará com que, no início do jogo, o CheckBox1 não seja visível e que, por padrão, seja a vez das peças brancas jogarem.
Agora, vá até o evento TabuleiroMouseUp em seu código e adicione a seguinte linha no início do código:
Ou seja, se estiver no modo de edição, o evento TabuleiroMouseUp, que está sendo utilizado para analisar a validade das jogadas, não será realizado (Exit).
Volte para o Form principal, clique sobre seu Tabuleiro, vá até os eventos no Object Inspector e dê dois cliques sobre o evento TabuleiroMouseDown. Atualize o código que está dentro deste evento para:
Com essa modificação, verificamos se o modo de edição está ativado. Caso esteja, as peças digitadas no campo Edit que foi adicionado ao Formulário serão inseridas na casa do tabuleiro onde ocorreu o clique. Para remover peças de uma casa, utilizamos a verificação para determinar se o Edit está vazio, ou seja, "if Edit1.Text = '' then". Nesse caso, a casa do tabuleiro será substituída por uma posição vazia.
Se tudo estiver configurado corretamente, ao executar o programa, você poderá personalizar o tabuleiro da maneira que preferir, mesmo que isso resulte em configurações absurdas, como mostrado na figura a seguir (embora não seja recomendado fazer isso ??).
Agora vamos abordar uma das partes mais aguardadas deste capítulo: como conectar nosso jogo de xadrez a uma das engines mais poderosas, o Stockfish.
Para fazer isso, você deve primeiro baixar a versão mais recente (na data deste tutorial) do Stockfish. Você pode encontrar o download da versão mais recente do Stockfish AQUI. Após o download, extraia os arquivos para uma pasta de sua escolha e renomeie o executável dentro dessa pasta para "stockfish.exe". A alteração do nome é feita apenas por conveniência, para facilitar a integração com o código, pois vamos conectar a engine diretamente no código de nossa unidade. No entanto, você também pode manter o nome original do arquivo "stockfish" e procurá-lo usando um TOpenDialog, se preferir.
Ao executar o arquivo "stockfish.exe" clicando duas vezes, você abrirá uma janela de comando preta que aguarda comandos. Os comandos para usar o Stockfish estão disponíveis em seu site. Os mais importantes para o nosso exemplo incluem:
Embora você possa usar esses comandos diretamente na janela de comando do Stockfish e jogar contra a engine dessa forma, essa não é uma opção prática. Existem vários programas que permitem que você se conecte à engine e jogue contra ela usando uma interface gráfica. Alguns desses programas são pagos, outros podem ser complicados de usar, mas todos oferecem uma experiência mais amigável ao jogador.
No nosso exemplo, vamos criar um processo de conexão simples usando pipes, que permitem a comunicação entre dois aplicativos. Embora a implementação de pipes seja relativamente fácil, erros podem ocorrer devido às diferentes configurações de programas diferentes. Para facilitar a conexão por pipes, usaremos um componente em Delphi chamado TDosCommand. Você pode fazer o download deste componente AQUI. Após a instalação do componente, adicione-o ao seu Formulário principal.
Agora, clique duas vezes no Formulário principal para acessar o evento OnCreate. Vamos adicionar os comandos para a comunicação com o Stockfish. Veja o código modificado para o evento OnCreate abaixo:
Agora, vamos explicar os comandos que utilizamos para estabelecer a comunicação com o Stockfish. No código do evento OnCreate, definimos o caminho para o Stockfish no seu computador. No meu caso, o Stockfish está localizado no disco D, dentro da pasta "stockfish". Certifique-se de ajustar esse caminho para refletir a localização da sua engine Stockfish.
Após configurar o caminho, executamos o componente DosCommand e aguardamos 1 segundo (Sleep(1000)) antes de enviar os comandos. Nesse momento, enviamos os comandos que foram mencionados anteriormente.
O comando "position fen..." seguido de uma sequência de letras e números indica o estado inicial do tabuleiro. O "w" no início dessa sequência indica que é a vez das brancas (White) jogarem. O "KQkq" indica que o roque está disponível em todas as posições. O comando '-' indica que não há casa para a captura EnPassant disponível. A letra 'O' indica quantos meio movimentos foram realizados desde a última captura ou movimento do peão (isso é útil para determinar o empate por repetição, como vimos no Capítulo 6). O número 1 no final representa o total de movimentos que foram realizados até o momento, começando com o valor 1 no início.
É importante observar que, se você deseja editar o tabuleiro e ativar o modo de análise com inteligência artificial em nosso jogo de xadrez, deve ajustar o posicionamento FEN antes de prosseguir com o jogo.
Agora, retorne ao seu Formulário principal e adicione dois componentes TPanel, dois TLabel (um em cada TPanel) e quatro componentes TRadioButton (dois em cada TPanel). Modifique os textos exibidos nos seus componentes no Object Inspector para obter o seguinte resultado:
Veja que deixamos os RadioButton no modo "checked" no Object Inspector para a opção Humano e não Computador. Ou seja, o estado inicial do nosso tabuleiro vai ser um jogo entre Humano vs. Humano.
Volte para o unit e vá até o evento TabuleiroMouseUp. Adicione o seguinte código após a atualização do HistoricoMovimentos:
Neste ponto, é importante declarar duas variáveis globais, DepthB e DepthP, ambas do tipo Integer, juntamente com LinhaAI, do tipo String. As variáveis DepthB e DepthP serão utilizadas para indicar a profundidade de busca que o Stockfish deve realizar para as brancas e pretas, respectivamente. Isso determina o nível de "inteligência" da máquina. Já a variável LinhaAI será usada para estabelecer a comunicação por pipes diretamente com o Stockfish, atualizando o posicionamento do tabuleiro e enviando as informações corretas para a engine.
É importante mencionar que utilizamos nossa constante Tabu do tabuleiro para ler as posições das casas do tabuleiro. O formato da variável LinhaAI será algo como "position startpos moves d2d4 d7d5", onde cada vez que uma jogada for realizada, essa linha será atualizada para enviar as informações necessárias para a engine.
Para definir a profundidade de busca da engine, você pode adicionar dois componentes TSpinEdit e três componentes TLabel ao seu Formulário. Renomeie os captions dos Labels da seguinte forma:
Também, no Object Inspector, modifique o valor da propriedade Value dos SpinEdit conforme desejar para as peças brancas e pretas. É importante notar que você só precisa ajustar esse valor quando a engine estiver jogando como brancas ou pretas.
Agora, dê dois cliques no SpinEdit1 e adicione o seguinte código:
Faça a mesma coisa para o SpinEdit2:
Isso vai fazer com que as variáveis globais sejam atualizadas caso os valores no SpinEdit sejam modificados.
Agora vamos inicializar as variáveis globais DepthB e DepthP no evento OnCreate do Form. O seu evento OnCreate deve ficar da seguinte forma:
Vimos como enviar a mensagem para a engine, agora temos que receber a resposta. Para isto, volte ao seu formulário principal, selecione o componente DosCommand vá até os Eventos do Object Inspector e dê dois cliques sobre o evento OnNewLine. Adicione o seguinte código:
No código acima, recebemos a mensagem da engine pelo DosCommand através da string ANewLine. O melhor movimento é dado pela expressão "bestmove" seguido da posição sugerida pela engine. Com está informação recortamos a informação de qual é a peça que deve se mover e para onde ela deve ir.
Utilizamos um simples cálculo matemático para traduzir isso para nossas linhas e colunas. Por fim o comando é enviado para o nosso Tabuleiro, acionando o evento TabuleiroMouseUp.
Pronto, agora seu tabuleiro pode se comunicar efetivamente com a inteligência artificial do Stockfish. Veja no vídeo abaixo como deve ser comportar seu tabuleiro quando colocamos as duas inteligências artificiais para se enfrentarem.
Espero que tenham aproveitado este tutorial completo em sete capítulos sobre como criar um jogo de xadrez em Delphi. Em breve, estaremos disponibilizando o jogo completo para download em nosso site.
Lembrando que todos os códigos desenvolvidos e compartilhados aqui pertencem aos desenvolvedores do site e não devem ser utilizados para fins comerciais sem a devida autorização dos proprietários.
Caso tenham alguma dúvida ou precisem de assistência adicional, não hesitem em entrar em contato conosco. Estamos à disposição para ajudá-los.