Jogo da velha com Inteligência Artificial (Tic tac toe AI C#)



O que é inteligência artificial?
Antes de começar a aula, você sabe o que é uma Inteligência Artificial? (IA)
Este campo ainda está sendo estudado por diversos pesquisadores de todo o mundo, então não seria nesta postagem que eu poderia lhe definir tal mecanismo com todos os detalhes possíveis de forma perfeita, mas em resumo, a Inteligencia Artificial é um conjunto de escolhas/decisões realizadas através de softwares, que se assemelha ao cérebro humano, ou seja, através de conexões lógicas poderá resolver determinado problema, sempre visando na obtenção de sucesso do mesmo.
Claro que, o que iremos fazer no tutorial de hoje está muito longe do que foi dito acima, mesmo porque, temos diversos tipos de IAs em seus mais diversos níveis de "inteligência", desde aqueles mais simplórios criados para jogos simples (como em nosso caso) até aqueles mais sofisticados, como a inteligente Siri, assistente virtual da Apple.
Sabendo disto, o que iremos programar neste tutorial é uma IA básica, guiada apenas por escolhas baseadas nas principais estratégias do jogo da velha, em outras palavras, vamos criar um mecanismo que apenas saberá jogar este jogo da forma mais natural possível.

Arregaçando as mangas:
Sabendo um pouco sobre a IA, vamos começar nosso trabalho e desenvolver a dita cuja! Porém, antes de tudo, recomendo altamente que veja nosso primeiro tutorial sobre a implementação de um jogo da velha em C# clicando neste link.
Isso é necessário pois, utilizaremos diversos comandos já feitos neste sistema, e muita da lógica aplicada naquele tutorial voltará a ser utilizado por aqui, portanto, antes de acompanhar esta postagem, dê uma olhada rápida no tutorial linkado anteriormente.
Aviso dado, vamos começar!
Vamos desenvolver o design do formulário da mesma forma que fizemos no tutorial anterior, sem precisar registrar os pontos dessa vez (caso não deseje):


Lembrando que os buttons serão as lacunas do jogo (formando a grade 3x3) e tem sua indexação correspondente inferida na propriedade "TabIndex" que irá nos servir de referência para sabermos qual botão foi preenchido, a fim de verificar possíveis empates ou vitórias.

Dê F7 no teclado e abra a janela de código do nosso jogo.
Vamos criar exatamente as mesmas variáveis que temos presente do tutorial anterior: o vetor de string que armazenará os textos dos botões, a variável inteira que irá guardar as rodadas jogadas, e as variáveis lógicas que determinarão o turno dos jogadores, se o jogo terminou, e uma que iremos utilizar para este tutorial que irá checar se a IA já terminou de jogar.
Podemos também instanciar a classe Random:




Lembrando que assim como no tutorial anterior, definimos que para o turno = true estará na vez do jogador (X) e no turno = false, estará na vez do jogador (O) que seria a IA.

Vamos então trazer o código da checagem de vencedor que utilizamos no tutorial passado para nosso código:


void ChecarVencedor() { // Horizontais: string Vazio = turno ? "X" : "O"; for (int i = 0; i <= 6; i += 3) { if (textos[i] == Vazio && textos[i] == textos[i + 1] && textos[i] == textos[i + 2]) { Vencedor(); } } // Verticais: for (int i = 0; i <= 2; i++) { if (textos[i] == Vazio && textos[i] == textos[i + 3] && textos[i] == textos[i + 6]) { Vencedor(); } } // Diagonais: if (textos[0] == Vazio && textos[0] == textos[4] && textos[0] == textos[8]) { Vencedor(); }; // Diagonal principal if (textos[2] == Vazio && textos[2] == textos[4] && textos[2] == textos[6]) { Vencedor(); }; // Diagonal secundária // Empate: if (rodadas == 9 && !fimDeJogo) { fimDeJogo = true; MessageBox.Show("Empate!"); } } void Vencedor() { fimDeJogo = true; MessageBox.Show(String.Format("Jogador {0} venceu!", turno ? "X" : "O")); }

Programando botões:
Vamos agora programar os botões para que o jogador possa clicar neles e marcar um X nos mesmos.
A lógica implementada neste método será basicamente a mesma utilizada no outro tutorial, com a diferença que não haverá necessidade de verificar a vez do jogador, visto que ele só poderá jogar quando "turno" for verdadeiro:

Não se esqueça de retornar ao design do formulário e selecionar todos os botões que formam as lacunas do jogo e em "Events >> Click" selecionar o método "Botoes" que criamos, fazendo assim com que ao clicar em qualquer botão que seja, este método seja disparado:



Princípio de decisão da IA:
Agora vamos começar a programar a nossa inteligência artificial. Para iniciarmos o seu desenvolvimento, pense em como uma pessoa real faria para jogar este jogo: primeiramente iria tentar verificar se existe alguma possibilidade de vitória, fazendo com que marque no local correto para ganhar, ou seja, um pensamento ofensivo. Caso perceba-se que não há chances eminentes de vencer, iria tomar a decisão de verificar se existe chances do oponente ganhar, fazendo com que marque no local correto para impedir a sua vitória, ou seja, uma decisão defensiva. Por fim, se fosse notado que ninguém tem chances de ganhar neste momento, a próxima decisão seria marcar em um local aleatório (normalmente no início do jogo e no final, quando dará empate de qualquer forma).
Concorda que com exceção de existir sempre uma estratégia fixa (o que não queremos para nossa IA) estes são os passos lógicos que tomamos quando jogamos este jogo? Exatamente nessa mesma ordem de raciocínio?
Então o que faremos será exatamente assim, primeiramente a IA vai tentar atacar, depois defender e em último caso jogar aleatoriamente.
Para cada uma dessas decisões lógicas teremos métodos implementados para realizar essas checagens, portanto podemos começar a fazer um deles.

Pensando aleatoriamente:
Vamos primeiramente desenvolver o método que faz com que a inteligência artificial jogue numa posição aleatória.
O mais importante neste método é sempre conferir se o botão que ela deseja jogar (que foi sorteado através da instância Random) está vazio.
Para isso, devemos criar um laço de repetição que irá rodar enquanto a IA não tenha achado um local correto para marcar, porém caso encontre, este laço parará. Isso será feito com base de uma variável booleana.
Também devemos realizar um laço foreach para percorrer todos os botões e verificar se aquele botão está vazio e se possui a TabIndex igual ao número sorteado (de 0 à 8) para que a IA escolha este botão para marcar a bolinha (O):


Vamos agora criar um método que será utilizado para marcar de fato a bolinha (O) no botão indicado, com a lógica bem similar ao método do botão do jogador.
Para isto, este método deve ter como parâmetro um inteiro que irá ser passado como a TabIndex correspondente do botão a ser marcado. Também vamos colocar a bolinha(O) como texto deste botão, armazenar esta informação do vetor de textos, realizar um incremento em "rodadas", fazer com que a variável "IAJogou" se torne verdadeira, chamar o método para checar a vitória, e por fim, trocar o turno para que o jogador possa voltar a jogar:


Chame agora este método que acabamos de criar dentro do método da aleatoriedade, passando como argumento justamente o valor sorteado:



Atacando e defendendo:
Tendo criado o pensamento aleatório da IA vamos agora desenvolver as tomadas de decisões conforme as chances da IA poder vencer ou perder.
Crie o método com uma passagem de parâmetro do tipo string que irá definir se a IA está verificado a chance de ganhar ou a chance de perder.
Ao chamar este método, podemos utilizá-lo para as duas validações: quando for passado como argumento a bolinha (O) a IA vai checar se pode vencer, verificando apenas os botões que possuam esta marcação. Quando este método for chamado com o argumento xis(X) ela vai verificar se pode perder, checando os botões apenas com "X" marcados:



Vamos agora analisar as possíveis formas da IA vencer (ou impedir que o jogador vença) com base das seguintes marcações durante o jogo:
Primeiro pelas horizontais:existem três formas distintas de vencer (incluindo as 3 linhas):

Respectivamente: Marcando nas primeira e segunda colunas, impedindo/vencendo marcando na terceira; ou na primeira e terceira coluna, vencendo/impedindo na segunda e por fim, marcando nas duas últimas colunas e colocando na primeira para impedir que o jogador vença, ou para ganhar.

Como temos três chances distintas de ganhar ou defender, podemos iniciar um loop que repita exatamente três vezes para validarmos essas três chances.
A partir desse laço, podemos verificar qual seu valor de incremento para então passarmos alguns valores para quatro variáveis locais.
Quando este loop estiver igual a 1, estamos representando aquela primeira chance de vencer, ou seja, marcando nas duas primeiras colunas.
Criando um segundo laço, vamos manipular seus valores de início, fim e validações dos botões para determinar a primeira possibilidade de vitória.
Podemos fazer o segundo laço percorrer cada texto dos botões de acordo com a sua linha (incrementando de 3 em 3).
Na primeira possibilidade de vencer, checaremos o primeiro botão e o segundo (o botão ao lado) e a chance de vencer surge marcando no terceiro botão.
Ou seja, este laço se inicia em 0, vai até 6, o botão ao lado a se verificar é contador + 1( ou seja, se tiver igual a 0 vai checar 1, se tiver em 3 vai verificar 4 e se tiver em 6 vai checar 7), portanto, a variável de auxílio "verificarAoLado" deve ser 1, e a de argumento deve ser 2, ou seja contador do loop + 2 (se tiver igual a 0 vai validar 2, sendo 3 validará 5 e sendo 6 validará 8).
Lembrando que estes números citados são referentes ao index de cada botão dentro do vetor.

Caso o primeiro laço esteja igual a 1, nós estamos validando a segunda chance de vencer, ou seja, quando os primeiro e último botões estiverem marcados, ganhará marcando no segundo.
A variável "verificarAoLado" então deve ser 2 e o argumento 1.

Por fim, quando este laço for 3, estaremos checando a terceira chance de vencer: Marcando nas duas últimas colunas, logo, o início do outro loop deve ser 1, seu limite deve ser 7, verificarAoLado será1 e argumento -1, para validar o primeiro botão da linha correspondente.
O método ficará assim:



Veja que podemos também vencer nas verticais das seguintes formas:


Perceba que também são três formas distintas de vencer, portanto podemos utilizar a mesma lógica que utilizamos para o problema anterior, só alterando as variáveis dos contadores e auxílio:



Em relação às duas diagonais, temos essas chances de vitórias:


Perceba que no caso das duas diagonais temos ao todo seis chances de vencer, três de cada uma, portanto, teremos um modo diferente de lidar com esta situação, apenas utilizando um loop que repita 6 vezes:



Perceba que na situação das diagonais nós utilizamos apenas o laço de repetição que representa as possibilidades de vencer, e a validação para todas as possíveis diagonais é sempre a mesma.
Outro fator a se destacar é que o código ficou compacto e dinâmico ao utilizar o vetor e os laços de repetições, e apesar de você poder discordar num primeiro momento, pense: São três formas de ganhar na horizontal contando as três linhas + três formas de vencer na vertical contando as três colunas + as seis formas de ganhar pelas duas diagonais, totalizando 24 maneiras de vencer.
Imagine ter de realizar 24 validações para este sistema de IA, se não fosse utilizado o laço de repetição e suas respectivas variáveis auxiliares? Seria um trabalho bem desnecessário.

Fazendo a IA pensar:
Nós programamos as validações que a IA fará com base das chances de perder ou vencer, mas você já parou para notar que ainda não chamamos esses métodos em momento algum?
O que precisamos fazer agora é criar um método que organize as decisões que a inteligência artificial irá tomar, ou seja, se ela vai atacar, defender ou randomizar.
Lembra-se da ordem de decisões? Pois então, podemos criar o método que repita um loop três vezes, e validarmos se a IA ainda não achou um local para jogar, e caso não tenha achado ele vai verificar o contador deste loop.
Quando ele estiver em 0, chamaremos o método de validação que criamos anteriormente passando como argumento a bolinha (O); caso a IA não tenha chances de vencer, ela precisa perceber se pode perder, então agora chamaremos o método passando como argumento o xis (X) e se ainda a IA perceber que ninguém pode ganhar, ela vai jogar aleatoriamente, chamando o método Random.
Podemos também finalizar este loop imediadamente caso a IA já tenha jogado, para melhor performance e para evitar possíveis bugs:


Agora você pode chamar este método dentro do método dos botões, quando o jogador termina sua jogada:


Programando o botão de limpar:
Por fim, para terminar este tutorial com chave de ouro, podemos realizar a programação do botão de limpar, dando dois cliques no mesmo a partir do design de formulário:


Através deste método estamos fazendo com que as variáveis voltem ao seu estado inicial, os dois laços estão limpando os textos dos botões e do vetor, respectivamente, o texto do botão de limpar volta a ser "recomeçar" e checamos se o turno inicia com a IA, chamando o seu método de pensar.

Finalização:
O resultado final será mais ou menos isto:




E finalmente chegamos mais uma vez no final de um tutorial por aqui! Eu decidi fazê-lo pois algumas pessoas me pediram bastante e também porque é muito bom para treinar a lógica e entender um pouco como alguma IA possa funcionar basicamente.
Espero que tenham gostado e até o próximo!

Comentários