Aprenda Inteligência Artificial para jogar um jogo

Bom dia, querido leitor!

Neste artigo, desenvolveremos uma rede neural que pode passar um jogo criado especialmente para ele em um bom nível.



Nota: este artigo não explica o termo " rede neural " e tudo relacionado a ele, nem fornece informações básicas sobre o treinamento em rede usando o método de rastreamento . Recomendamos que você se familiarize brevemente com esses conceitos antes de ler este artigo.

Início: descrição das regras do jogo


O objetivo é pegar o maior número possível de círculos com uma borda verde, evitando círculos com vermelho.

Termos:

  • O dobro de círculos vermelhos voa no campo como círculos verdes;
  • Círculos verdes permanecem dentro do campo, círculos vermelhos voam para fora do campo e são eliminados;
  • Círculos verdes e vermelhos podem colidir e repelir com sua própria espécie;
  • Jogador - uma bola amarela na tela, pode se mover dentro do campo.

Depois de tocar a bola desaparece, e o jogador recebe os pontos correspondentes.

Próximo: projetando AI


Receptores


Para que a IA possa decidir para onde mover o player, você primeiro precisa obter dados ambientais. Para fazer isso, crie scanners especiais que são linhas retas. O primeiro scanner está localizado em um ângulo de 180 graus em relação ao limite inferior do campo.

Haverá 68: os 32 primeiros - respondem a bombas (círculos vermelhos), os 32 seguintes - respondem a maçãs (círculos verdes) e os 4 últimos - recebem dados sobre a localização dos limites do campo em relação ao jogador. Vamos chamar esses 68 scanners de neurônios de entrada da futura rede neural (camada receptora).

O comprimento dos primeiros 64 scanners é de 1000 px, os 4 restantes correspondem à divisão do campo pela metade, de acordo com a face de simetria correspondente
imagem Escopo AI (1/4)
imagem

Na figura: neurônios de entrada que recebem valores dos scanners
Os valores nos scanners são normalizados, ou seja, reduzido para o intervalo [0, 1], e quanto mais próximo o objeto que cruza o scanner estiver do player, maior será o valor transmitido ao scanner.

Algoritmo para recebimento de dados por scanners e sua implementação em JS
Portanto, o scanner é direto. Temos as coordenadas de um de seus pontos (coordenadas do jogador) e o ângulo em relação ao eixo OX, podemos obter o segundo ponto usando as funções trigonométricas sin e cos.

A partir daqui, obtemos o vetor de direção da linha, o que significa que podemos construir a forma canônica da equação dessa linha.

Para obter o valor no scanner, você precisa ver se alguma bola cruza essa linha, o que significa que você precisa apresentar a equação da linha em uma forma paramétrica e substituir tudo isso na equação do círculo, sequencialmente para cada círculo do campo.

Se levarmos essa substituição a uma forma geral, obteremos uma equação paramétrica em relação a a, bec, onde essas variáveis ​​são os coeficientes da equação quadrática, começando pelo quadrado, respectivamente.

Convidamos o leitor a se familiarizar com esse algoritmo em mais detalhes usando as definições mais simples de álgebra linear
Abaixo está o código dos scanners

Ball.scaners: { // length: 1000, i: [], //   get_data: function(){ //Ball.scaners.get_data /     var angl = 0; var x0 = Ball.x, var y0 = Ball.y; var l = Ball.scaners.length; for(let k = 0; k < 32; k++){ x1 = l*Math.cos(angl), y1 = l*Math.sin(angl); Ball.scaners.i[k] = 0; for(i = 0; i < bombs.length; i++){ if(((k >= 0) && (k <= 8) && (bombs[i].x < x0) && (bombs[i].y < y0)) || ((k >= 8) && (k <= 16) && (bombs[i].x > x0) && (bombs[i].y < y0)) || ((k >= 16) && (k <= 24) && (bombs[i].x > x0) && (bombs[i].y > y0)) || ((k >= 24) && (k <= 32) && (bombs[i].x < x0) && (bombs[i].y > y0))){ //    var x2 = bombs[i].x, y2 = bombs[i].y; var p = true; //    var pt = true; //     var t1, t2; var a = x1*x1 + y1*y1, b = 2*(x1*(x0 - x2) + y1*(y0 - y2)), c = (x0 - x2)*(x0 - x2) + (y0 - y2)*(y0 - y2) - bombs[i].r*bombs[i].r; //------------------------------    if((a == 0) && (b != 0)){ t = -c/b; pt = false; } if((a == 0) && (b == 0)){ p = false; } if((a != 0) && (b != 0) && (c == 0)){ t1 = 0; t2 = b/a; } if((a != 0) && (b == 0) && (c == 0)){ t1 = 0; pt = false; } if((a != 0) && (b == 0) && (c != 0)){ t1 = Math.sqrt(c/a); t2 = -Math.sqrt(c/a); } if((a != 0) && (b != 0) && (c != 0)){ var d = b*b - 4*a*c; if(d > 0){ t1 = (-b + Math.sqrt(d))/(2*a); t2 = (-b - Math.sqrt(d))/(2*a); } if(d == 0){ t1 = -b/(2*a); } if(d < 0){ p = false; } } //----------------------------------- if(p == true){ if(pt == true){ let x = t1*x1 + x0; let y = t1*y1 + y0; let l1 = Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2); x = t2*x1 + x0; y = t2*y1 + y0; let l2 = Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2); if(l1 <= l2){ Ball.scaners.i[k] += 1 - l1/(l*l); }else{ Ball.scaners.i[k] += 1 - l2/(l*l); } }else{ let x = t1*x1 + x0; let y = t1*y1 + y0; Ball.scaners.i[k] += 1 - (Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2))/(l*l); } }else{ Ball.scaners.i[k] += 0; } }else{ continue; } } angl += Math.PI/16; } //!---------------  for(k = 32; k < 64; k++){ x1 = l*Math.cos(angl), y1 = l*Math.sin(angl); Ball.scaners.i[k] = 0; for(i = 0; i < apples.length; i++){ if(((k >= 32) && (k <= 40) && (apples[i].x < x0) && (apples[i].y < y0)) || ((k >= 40) && (k <= 48) && (apples[i].x > x0) && (apples[i].y < y0)) || ((k >= 48) && (k <= 56) && (apples[i].x > x0) && (apples[i].y > y0)) || ((k >= 56) && (k <= 64) && (apples[i].x < x0) && (apples[i].y > y0))){ var x2 = apples[i].x, var y2 = apples[i].y; var p = true; //    var pt = true; //     var t1, t2; var a = x1*x1 + y1*y1, b = 2*(x1*(x0 - x2) + y1*(y0 - y2)), c = (x0 - x2)*(x0 - x2) + (y0 - y2)*(y0 - y2) - apples[i].r*apples[i].r; //------------------------------    if((a == 0) && (b != 0)){ t = -c/b; pt = false; } if((a == 0) && (b == 0)){ p = false; } if((a != 0) && (b != 0) && (c == 0)){ t1 = 0; t2 = b/a; } if((a != 0) && (b == 0) && (c == 0)){ t1 = 0; pt = false; } if((a != 0) && (b == 0) && (c != 0)){ t1 = Math.sqrt(c/a); t2 = -Math.sqrt(c/a); } if((a != 0) && (b != 0) && (c != 0)){ var d = b*b - 4*a*c; if(d > 0){ t1 = (-b + Math.sqrt(d))/(2*a); t2 = (-b - Math.sqrt(d))/(2*a); } if(d == 0){ t1 = -b/(2*a); } if(d < 0){ p = false; } } //----------------------------------- if(p == true){ if(pt == true){ let x = t1*x1 + x0; let y = t1*y1 + y0; let l1 = Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2); x = t2*x1 + x0; y = t2*y1 + y0; let l2 = Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2); if(l1 <= l2){ Ball.scaners.i[k] += 1 - l1/(l*l); }else{ Ball.scaners.i[k] += 1 - l2/(l*l); } }else{ let x = t1*x1 + x0; let y = t1*y1 + y0; Ball.scaners.i[k] += 1 - (Math.pow((x - Ball.x), 2)+Math.pow((y - Ball.y), 2))/(l*l); } }else{ Ball.scaners.i[k] += 0; } }else{ continue; } } angl += Math.PI/16; } Ball.scaners.i[64] = (1000 - Ball.x) / 1000; //  Ball.scaners.i[65] = Ball.x / 1000; //  Ball.scaners.i[66] = (500 - Ball.y) / 500; //  Ball.scaners.i[67] = Ball.y / 500; //  } } 

Projeto de rede neural


Portanto, era costume usar o algoritmo de rastreamento (caminhos de referência) para o treinamento de NS.
Nota: para simplificar o processo de aprendizagem, a construção de matrizes é omitida.

Prossiga:

  1. Na saída da rede, apresentamos 8 neurônios responsáveis ​​pelas direções: o primeiro é deixado, o segundo é deixado + topo, o terceiro é topo, o quarto é topo + direito, o quinto é direito e assim por diante;
  2. Permita que um valor positivo em um neurônio signifique que você deve se mover na direção que está ligada a ele e negativo - o oposto;
  3. É importante para nós, de alguma forma, combinar os valores dos scanners que estão no mesmo ângulo em relação à borda inferior do campo. Como com uma saída negativa a AI se repelirá a partir dessa direção, introduzimos uma camada adicional (oculta, entre a entrada e a saída), que adicionará os valores dos neurônios que refletem os scanners correspondentes em pares e tomaremos os valores dos neurônios vermelhos com um "-";
  4. Obviamente, o movimento para a esquerda é afetado principalmente pelas informações à esquerda do jogador (não apenas, mas isso será levado em consideração mais tarde (*)) - isso significa que conectamos 8 neurônios da camada oculta localizada à esquerda com o neurônio responsável por se mover para a esquerda. Estabelecemos as conexões da seguinte forma: um neurônio correspondente a um scanner localizado paralelamente ao limite do campo recebe um peso de 1, depois dois neurônios vizinhos recebem um peso de 0,95, os vizinhos passam por um único peso de 0,9 e, finalmente, o último nesse setor é de 0,8;
  5. Também levamos em conta os neurônios de fronteira: atraímos as comunicações desses neurônios para as saídas que podem influenciar o movimento das fronteiras.
  6. Faz o mesmo com os sete neurônios restantes da camada de saída.

Tendo completado o algoritmo acima, obtemos a rede neural mostrada na figura abaixo

imagem

* Mas isso não é tudo. É importante interpretar corretamente o resultado, a saber (Y é uma matriz de neurônios de saída):
  Ball.vx = -(Y[0] - Y[4]) + (-(Y[1] - Y[5]) + (Y[3] - Y[6]))*0.5; Ball.vy = -(Y[2] - Y[6]) + (-(Y[3] - Y[7]) + (Y[5] - Y[1]))*0.5; 


Assim, criamos e treinamos automaticamente a rede neural subjacente à IA do jogador no gif na parte superior do artigo

O código está disponível (implementação do jogo e IA) no link: Link para o github Ivan753 / Learning

Isso é tudo. Obrigado pela atenção!

Source: https://habr.com/ru/post/pt418895/


All Articles