Snake, Mouse e Hamilton

Bom Dia. Vamos ensinar o computador a brincar de cobra.



Na verdade, esta é a primeira parte de um artigo sobre como você pode tentar resolver esse problema. Vamos apenas dizer aquecimento, antes da luta principal.





Como tudo começou



Para ser totalmente franco, tudo começou assistindo a um vídeo de um australiano que se autodenomina " Code Bullet ".

Ele está tentando resolver um jogo simples de cobra usando vários algoritmos de IA.

Ele realmente não teve sucesso, e é por isso ... Os algoritmos atuais que a comunidade de IA fornece agora resolvem apenas dois problemas, Classificação ou Regressão (previsão). Mas a cobra não cabe ali nem ali. Pela ideia de que comer um rato é bom e bater é mau. Ele quebra na cauda, ​​que vai crescendo e crescendo constantemente até ocupar todo o campo. Portanto, parecia uma ideia legal escrever uma IA de autoaprendizagem completa para tal tarefa, mas primeiro decidi aquecer e escrever apenas um algoritmo que resolveria o problema de frente, sem treinamento. Na verdade, esse algoritmo será discutido.



P.S. O artigo não conterá grandes descobertas, mas ideias "emprestadas" de outros e sua própria implementação com uma história sobre o que veio e de onde.



Escrever um jogo



Antes de jogar, vamos escrever o próprio jogo.



Todos os cálculos serão feitos no servidor, a cobra será exibida no navegador, e as informações serão trocadas via WebSocket (SignalR).



Código chato sem o qual nada funciona
. Store .



    isLife: boolean,
    isWin: boolean,
    xSize: number,
    ySize: number,
    mouse: Vector2,
    piton: Vector2[]
      
      





snake.ts



import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
import { observable } from "mobx";
import { start } from "repl";

export enum Cell {
    None = "None",
    Mouse = "Mouse",
    Snake = "Snake"
}

export interface Vector2 {
    x: number,
    y: number
}

interface StateBoard {
    isLife: boolean,
    isWin: boolean,
    xSize: number,
    ySize: number,
    mouse: Vector2,
    piton: Vector2[]
    hamiltonPath: Vector2[]
}

class Snake {
    @observable
    public state?: StateBoard;

    private connection: HubConnection;

    constructor() {
        this.connection = new HubConnectionBuilder()
            .withUrl("http://localhost:5000/snake")
            .withAutomaticReconnect()
            .build();
        this.start();
    }

    private start = async () => {
        await this.connection.start();
        this.connection.on("newState", (board: string) => {
            var state = JSON.parse(board) as StateBoard;
            if (state.isLife) {
                this.state = state;
            } else {
                this.state!.isLife = false;
                this.state!.isWin = state.isWin;
                if (this.state!.isWin) {
                    this.state = state;
                }
            }
        });
    }
}
export const snake = new Snake();
      
      





React , .



App.tsx



import { snake } from './shores/snake';
import { useObserver } from 'mobx-react-lite';
import cs from 'classnames';

const App = (): JSX.Element => {

  const cellRender = (indexRow: number, indexColumn: number): JSX.Element => {
    const state = snake.state!;
    const isMouse = state.mouse.x == indexColumn && state.mouse.y == indexRow;
    if (isMouse) {
      return <div key={`${indexRow}_${indexColumn}`} className='cell mouse'></div>;
    }
    const indexCellSnake = state.piton.findIndex(p => p.x == indexColumn && p.y == indexRow);
    if (indexCellSnake == -1) {
      return <div key={`${indexRow}_${indexColumn}`} className='cell'></div>;
    }
    const prewIndexCellSnake = indexCellSnake - 1;
    const prewCellPiton = 0 <= prewIndexCellSnake ? state.piton[prewIndexCellSnake] : null;
    const cellPiton = state.piton[indexCellSnake];
    const nextIndexCellSnake = indexCellSnake + 1;
    const nextCellPiton = nextIndexCellSnake < state.piton.length ? state.piton[nextIndexCellSnake] : null;
    let up = false, left = false, down = false, rigth = false;
    if (!!prewCellPiton) {
      if (prewCellPiton.x < cellPiton.x) {
        left = true;
      }
      if (prewCellPiton.y < cellPiton.y) {
        up = true;
      }
      if (cellPiton.x < prewCellPiton.x) {
        rigth = true;
      }
      if (cellPiton.y < prewCellPiton.y) {
        down = true;
      }
    }
    if (!!nextCellPiton) {
      if (cellPiton.x < nextCellPiton.x) {
        rigth = true;
      }
      if (cellPiton.y < nextCellPiton.y) {
        down = true;
      }
      if (nextCellPiton.x < cellPiton.x) {
        left = true;
      }
      if (nextCellPiton.y < cellPiton.y) {
        up = true;
      }
    }
    return <div key={`${indexRow}_${indexColumn}`} className='cell'>
      <table className='shake'>
        <tbody>
          <tr>
            <td width="10%" height="10%" />
            <td height="10%" className={cs({
              'shake-segment': up
            })} />
            <td width="10%" height="10%" />
          </tr>
          <tr>
            <td width="10%" className={cs({
              'shake-segment': left
            })} />
            <td className='shake-segment' />
            <td width="10%" className={cs({
              'shake-segment': rigth
            })} />
          </tr>
          <tr>
            <td width="10%" height="10%" />
            <td height="10%" className={cs({
              'shake-segment': down
            })} />
            <td width="10%" height="10%" />
          </tr>
        </tbody>
      </table>
    </div>;
  }

  const rowRender = (indexRow: number): JSX.Element => {
    const state = snake.state!;
    const cells: JSX.Element[] = [];
    for (let j = 0; j < state.ySize; j++) {
      cells.push(cellRender(indexRow, j));
    }
    return (<>{cells}</>);
  }

  const tableRender = (): JSX.Element => {
    const state = snake.state!;
    const rows: JSX.Element[] = [];
    for (let i = 0; i < state.ySize; i++) {
      rows.push(
        (<div key={i.toString()} className="row">
          {rowRender(i)}
        </div>)
      );
    }
    return (<>{rows}</>);
  }

  return useObserver(() => {
    console.log(snake.state);
    if (!snake.state) {
      return <div />
    }
    let state: string = " ";
    if (snake.state.isLife == false) {
      state = snake.state.isWin ? "" : ""
    }
    return (<>
      <span className="state">{state}</span>
      <div className="table">
        {tableRender()}
      </div>
    </>);
  });
}

export default App;
      
      





:



    public class SnakeSender
    {
        class Vector2
        {
            public int X { get; set; }
            public int Y { get; set; }
            public Vector2(int x, int y)
            {
                this.X = x;
                this.Y = y;
            }
        }

        class Vector2Comparer : IEqualityComparer<Vector2>
        {
            public bool Equals([AllowNull] Vector2 value1, [AllowNull] Vector2 value2)
            {
                return value1.X == value2.X && value1.Y == value2.Y;
            }

            public int GetHashCode([DisallowNull] Vector2 obj)
            {
                return 0;
            }
        }

        private readonly static Vector2Comparer vector2Comparer = new Vector2Comparer();


        [JsonConverter(typeof(StringEnumConverter))]
        enum Cell
        {
            None,
            Mouse,
            Snake
        }

        enum Directions
        {
            Up,
            Left,
            Down,
            Rigth
        }

        class StateBoard
        {
            public bool IsLife { get; set; }
            public bool IsWin { get; set; }
            public int XSize { get; set; }
            public int YSize { get; set; }
            public Vector2 Mouse { get; set; }
            public List<Vector2> Piton { get; set; }
            public List<Vector2> HamiltonPath { get; set; }
        }

        const int xSize = 12, ySize = 12;
      
...
private void CheckDead()
        {
            Vector2 head = this.Piton.Last();
            if (head.X < 0
             || head.Y < 0
             || xSize <= head.X
             || ySize <= head.Y
             || this.Piton.SkipLast(1).Contains(head, vector2Comparer))
            {
                this.IsLife = false;
                this.IsWin = false;
                return;
            }
        }
 private void Render()
        {
            var hubContext = (IHubContext<SnakeHub>)this.ServiceProvider.GetService(typeof(IHubContext<SnakeHub>));
            var piton = this.Piton.ToList();
            piton.Reverse();
            hubContext.Clients?.All.SendAsync("newState", JsonConvert.SerializeObject(new StateBoard()
            {
                IsLife = this.IsLife,
                IsWin = this.IsWin,
                XSize = xSize,
                YSize = ySize,
                Mouse = this.Mouse,
                Piton = piton,
                HamiltonPath = HamiltonPath
            }));
        }
private List<Vector2> GetEmptyCells()
        {
            List<Vector2> emptyCells = new List<Vector2>(xSize * ySize);
            for (int i = 0; i < ySize; i++)
            {
                for (int j = 0; j < xSize; j++)
                {
                    if (!this.Piton.Contains(new Vector2(j, i), vector2Comparer))
                    {
                        emptyCells.Add(new Vector2(j, i));
                    }
                }
            }

            return emptyCells;
        }
}
      
      







No início do jogo, temos um rato vermelho e uma cobra unicelular.



E agora, de alguma forma, precisamos começar a jogar.



Em geral, brincar de cobra é muito simples - você só precisa passar por cada célula da matriz uma vez. E todo o problema está resolvido - Happy End.



Para ser mais preciso, nosso campo é um "Gráfico Conectado". Aqueles. cada célula do tabuleiro é um vértice. As arestas vão de cada vértice - esta é uma transição para um vértice adjacente.

Existem 2 a 4 dessas bordas. Para a célula extrema e para a central, respectivamente.



Em algum lugar de 4 de agosto de 1805 - 2 de setembro de 1865, um certo Hamilton William Rowan vivia na Irlanda, investigou o problema de "viajar ao redor do mundo" ao longo do dodecaedro. Nesse problema, os vértices do dodecaedro simbolizavam cidades famosas como Bruxelas, Amsterdã, Edimburgo, Pequim, Praga, Delhi, Frankfurt etc., e as bordas simbolizavam as estradas que as conectavam. O viajante deve dar “a volta ao mundo”, encontrando um caminho que passa por todos os picos exatamente uma vez. Para tornar a tarefa mais interessante, a ordem de passagem das cidades foi estabelecida com antecedência. E para ficar mais fácil lembrar quais cidades já estavam conectadas, um prego foi cravado em cada vértice do dodecaedro, e o caminho pavimentado foi marcado com uma pequena corda que poderia ser enrolada em volta do prego. No entanto, essa construção acabou sendo muito complicada, e Hamilton propôs uma nova versão do jogo, substituindo o dodecaedro por um gráfico plano,isomórfico ao gráfico construído nas bordas do dodecaedro.



Em geral, existe uma piada como "ciclo hamiltoniano". Um ciclo hamiltoniano "é um ciclo (caminho fechado) que passa por cada vértice de um determinado gráfico exatamente uma vez; ou seja, um ciclo simples que inclui todos os vértices do gráfico. Também intimamente relacionado a um gráfico hamiltoniano está a noção de um caminho hamiltoniano, que é um caminho simples (caminho sem loops) passando por cada vértice do gráfico exatamente uma vez. Um caminho hamiltoniano difere de um ciclo porque os pontos inicial e final do caminho podem não coincidir, ao contrário de um ciclo. O ciclo hamiltoniano é o caminho hamiltoniano.



Visualmente, isso pode ser representado como





No nosso caso, sim.





Só aqui está uma nuance ... Se tentarmos construir tal ciclo a partir da escavadeira, obteremos uma enumeração de tantas opções que será possível esperar até a segunda vinda.



O resultado final é que a abordagem geral para encontrar o "ciclo hamiltoniano" envolve uma pesquisa exaustiva e parece não haver nada mais ideal. E temos, com uma matriz de 12 por 12, apenas 144 vértices e para cada um precisamos verificar de 2 a 4 arestas. Em geral, existe em algum lugar a complexidade de n! ..



Mas desde resolvermos um problema para uma matriz na qual cada vértice está conectado a todos os vizinhos, você pode apenas fazer um passe no sentido horário.



Então não é difícil construir um "ciclo hamiltoniano".



private void CreateHamiltonPath()
        {
            this.HamiltonPath.Clear();
            this.HamiltonPath.Add(new Vector2(0, 0));
            HamiltonStep(this.HamiltonPath.Last());
        }

        private bool HamiltonStep(Vector2 current)
        {
            if (HamiltonPath.Count == HamiltonPath.Capacity)
            {
                var first = HamiltonPath.First();
                return (first.X == current.X && first.Y == current.Y - 1)
                    || (first.X == current.X && first.Y == current.Y + 1)
                    || (first.X - 1 == current.X && first.Y == current.Y)
                    || (first.X + 1 == current.X && first.Y == current.Y);
            }
            foreach (var direction in new[] { Directions.Down, Directions.Rigth, Directions.Up, Directions.Left })
            {
                Vector2 newElement = null;
                switch (direction)
                {
                    case Directions.Up:
                        newElement = new Vector2(current.X, current.Y - 1);
                        break;
                    case Directions.Left:
                        newElement = new Vector2(current.X - 1, current.Y);
                        break;
                    case Directions.Down:
                        newElement = new Vector2(current.X, current.Y + 1);
                        break;
                    case Directions.Rigth:
                        newElement = new Vector2(current.X + 1, current.Y);
                        break;
                }
                if (0 <= newElement.X && newElement.X < xSize
                    && 0 <= newElement.Y && newElement.Y < ySize
                    && !HamiltonPath.Contains(newElement, vector2Comparer))
                {
                    HamiltonPath.Add(newElement);
                    if (HamiltonStep(newElement))
                    {
                        return true;
                    }
                    HamiltonPath.Remove(newElement);
                }
            }
            return false;
        }
      
      





E sim, eu "peguei emprestado" essa ideia de " Code Bullet ", e ele pegou de outro cara na Internet.



Em suma, como disse Pablo Picasso:

Bons artistas copiam, grandes artistas roubam






E assim, temos uma cobra que dá um ciclo até a vitória:





Em geral, o problema está resolvido! Mas como fica horrível quando uma cobra unicelular rasteja de um ponto para o outro lado. Embora não haja obstáculos para pegá-lo ...



E do ponto de vista da matemática, temos a complexidade em (O) n ^ 2. Onde n é o número de pontos (em nosso caso n = 144).



Porque toda vez ... todo ... Precisamos dar a volta em tudo em um loop ...



Simplificando - vamos nos cansar de esperar ... Então, embora a solução seja boa, há um problema - leva muito tempo ...



Otimização



O que sabemos sobre a cobra. Seu comprimento muda quando o mouse é comido. E a trajetória ideal para ela é o caminho de Hamelton. E a distância mais curta entre dois pontos é uma linha reta.



Vamos começar chamando a recursão:



private void CalculatePath()
        {
            this.StepsCountAfterCalculatePath = 0;
            int finalIndexPoint = this.HamiltonPath.FindIndex(p => p.X == this.Mouse.X && p.Y == this.Mouse.Y);
            List<Vector2> tempPath = new List<Vector2>();
            List<Vector2> stepPiton = new List<Vector2>(this.Piton);
            Debug.WriteLine($"Piton length: {this.Piton.Count}");
            int index = 0;
            var result = StepTempPath(ref index, GetInvert(stepPiton, this.Mouse), this.Piton.Last(), finalIndexPoint, stepPiton, tempPath);
            if (result.PathIsFound)
            {
                this.TempPath = new Queue<Vector2>(tempPath);
                this.InvertHamiltonPath = result.InvertHamiltonPath;
            }
        }
      
      





Mas vamos analisar a parte recursiva separadamente.



A parte principal é o mais simples possível.



Estamos nos aproximando obstinadamente do objetivo:



if (current.X < finalPoint.X)
                {
                    newElement = new Vector2(current.X + 1, current.Y);
                }
                else if (finalPoint.X < current.X)
                {
                    newElement = new Vector2(current.X - 1, current.Y);
                }
                else if (current.Y < finalPoint.Y)
                {
                    newElement = new Vector2(current.X, current.Y + 1);
                }
                else if (finalPoint.Y < current.Y)
                {
                    newElement = new Vector2(current.X, current.Y - 1);
                }
      
      





A cobra não sabe como rastejar diagonalmente, mas agora primeiro alinhamos verticalmente e depois vamos para o gol horizontalmente. E então você pode ir para uma nova iteração para verificar.



A verificação de estado final parece algo assim para começar.



if (current.X == finalPoint.X && current.Y == finalPoint.Y)
            {
                var tempPiton = stepPiton.TakeLast(this.Piton.Count).ToList();
                for (int i = 1; i < this.Piton.Count; i++)
                {
                    var hamiltonPoint = (finalIndexPoint + i < this.HamiltonPath.Count) ? this.HamiltonPath[finalIndexPoint + i] : this.HamiltonPath[finalIndexPoint + i - this.HamiltonPath.Count];
                    if (tempPiton.TakeLast(this.Piton.Count).Contains(hamiltonPoint, vector2Comparer))
                    {
                        return false;
                    }
                    tempPiton.Add(hamiltonPoint);
                }
                return true;
            }
      
      





O que estamos realmente fazendo aqui.



Quando encontramos nosso mouse no caminho virtual. Além disso, continuamos a construir um caminho, mas desta vez ao longo do caminho ideal de Hamilton, e se ele não se cruzar com o corpo de nossa cobra, então sabemos com certeza que ao longo deste caminho até o rato atual, você pode com segurança deixe a cobra ir, já que depois de "comer um rato", onde quer que esteja o próximo, podemos percorrer o caminho de um ciclo completo e comer o próximo.



Enquanto formos unicelulares, não pode haver nenhum problema, em princípio, mas isso não dura muito ... Assim que nosso comprimento se torna mais do que um caminho direto para a meta, ele cria um problema. O ciclo tem uma determinada direção, ou seja, a cobra sempre se move no sentido horário, pois na verdade custamos o próprio caminho.



E é aí que surge o problema. Digamos que cheguemos ao próximo mouse a partir do topo e deixemos Hamilton subir neste local específico do caminho. A cobra se moverá contra si mesma, o que é impossível em princípio ... Para resolver este problema, apresentaremos o conceito de um caminho invertido de Hamilton.



Aqueles. a cobra pode se mover não apenas no sentido horário ao longo do qual o caminho foi construído, mas também na direção oposta ao longo desse caminho. O caminho não mudará a partir disso, mas a direção do movimento - sim.



Vamos mudar o cheque.



if (current.X == finalPoint.X && current.Y == finalPoint.Y)
            {
                if (this.Piton.Count == 1)
                {
                    return new ResultAnlaizePath(true);
                }
                foreach (var d in new[] { false, true })
                {
                    var tempPiton = stepPiton.TakeLast(this.Piton.Count).ToList();
                    bool isFound = true;
                    bool invertHamiltonPath = d;
                    for (int j = 1; j < this.Piton.Count; j++)
                    {
                        Vector2 hamiltonPoint;
                        if (invertHamiltonPath)
                        {
                            hamiltonPoint = (finalIndexPoint - j >= 0) ? this.HamiltonPath[finalIndexPoint - j] : this.HamiltonPath[this.HamiltonPath.Count - j];
                        }
                        else
                        {
                            hamiltonPoint = (finalIndexPoint + j < this.HamiltonPath.Count) ? this.HamiltonPath[finalIndexPoint + j] : this.HamiltonPath[finalIndexPoint + j - this.HamiltonPath.Count];
                        }
                        if (tempPiton.TakeLast(this.Piton.Count).Contains(hamiltonPoint, vector2Comparer))
                        {
                            isFound = false;
                            break;
                        }
                        tempPiton.Add(hamiltonPoint);
                    }
                    if (isFound)
                    {
                        return new ResultAnlaizePath(true, invertHamiltonPath);
                    }
                }
                return new ResultAnlaizePath(false);
            }
      
      





E por falar nisso, o referido "Código Bullet" não pensou nesta finta com os ouvidos. Estou falando em inverter, mas ele "cortou cantos", de acordo com algum de seu algoritmo, que permaneceu um segredo coberto pela escuridão. Mas posso dizer com certeza que houve uma articulação em sua decisão, por causa da qual sua passagem falhou.







Bem, em geral, o que mais posso dizer. É claro que além de se opor ao movimento da cobra, você pode simplesmente entrar em sua cauda. Para contornar essa situação, escreveremos uma busca simples por outra opção de caminho.



if (!stepPiton.TakeLast(this.Piton.Count).Contains(newElement, vector2Comparer))
            {
                tempPath.Add(newElement);
                stepPiton.Add(newElement);
                var retult = StepTempPath(ref index, !invert, newElement, finalIndexPoint, stepPiton, tempPath);
                if (retult.PathIsFound)
                {
                    return retult;
                }
                if (this.HamiltonPath.Count < index)
                {
                    return new ResultAnlaizePath(false);
                }
                tempPath.Remove(newElement);
                stepPiton.Remove(newElement);
            }

            Vector2 nextFinalPoint;
            if (this.InvertHamiltonPath)
            {
                nextFinalPoint = (finalIndexPoint - 1 < 0) ? this.HamiltonPath[this.HamiltonPath.Count - 1] : this.HamiltonPath[finalIndexPoint - 1];
            }
            else
            {
                nextFinalPoint = (finalIndexPoint + 1 == this.HamiltonPath.Count) ? this.HamiltonPath[0] : this.HamiltonPath[finalIndexPoint + 1];
            }
            List<Directions> directions = new List<Directions>(4);
            directions.Add(finalPoint.Y < nextFinalPoint.Y ? Directions.Up : Directions.Down);
            directions.Add(finalPoint.X < nextFinalPoint.X ? Directions.Left : Directions.Rigth);
            directions.Add(finalPoint.Y < nextFinalPoint.Y ? Directions.Down : Directions.Up);
            directions.Add(finalPoint.X < nextFinalPoint.X ? Directions.Rigth : Directions.Left);

            foreach (var direction in directions)
            {
                switch (direction)
                {
                    case Directions.Up:
                        newElement = new Vector2(current.X, current.Y - 1);
                        break;
                    case Directions.Left:
                        newElement = new Vector2(current.X - 1, current.Y);
                        break;
                    case Directions.Down:
                        newElement = new Vector2(current.X, current.Y + 1);
                        break;
                    case Directions.Rigth:
                        newElement = new Vector2(current.X + 1, current.Y);
                        break;
                }
                if (0 <= newElement.X && newElement.X < xSize
                 && 0 <= newElement.Y && newElement.Y < ySize
                 && !stepPiton.TakeLast(this.Piton.Count).Contains(newElement, vector2Comparer))
                {
                    tempPath.Add(newElement);
                    stepPiton.Add(newElement);
                    var retult = StepTempPath(ref index, GetInvert(stepPiton, finalPoint), newElement, finalIndexPoint, stepPiton, tempPath);
                    if (retult.PathIsFound)
                    {
                        return retult;
                    }
                    if (this.HamiltonPath.Count < index)
                    {
                        return new ResultAnlaizePath(false);
                    }
                    tempPath.Remove(newElement);
                    stepPiton.Remove(newElement);
                }
            }
            return new ResultAnlaizePath(false);
      
      





Em geral, nada complicado.



Nós podemos apenas insistir nisso.



Aqui estou tentando deixar a busca na direção oposta ao "movimento para frente" do mouse descrito acima.



            List<Directions> directions = new List<Directions>(4);
            directions.Add(finalPoint.Y < nextFinalPoint.Y ? Directions.Up : Directions.Down);
            directions.Add(finalPoint.X < nextFinalPoint.X ? Directions.Left : Directions.Rigth);
            directions.Add(finalPoint.Y < nextFinalPoint.Y ? Directions.Down : Directions.Up);
            directions.Add(finalPoint.X < nextFinalPoint.X ? Directions.Rigth : Directions.Left);

      
      





Aqueles. “Dê um passo para o outro lado” para contornar o obstáculo que se encontrava na reta.



Abaixo está o código completo, mas ele poderia ter sido dividido em arquivos para ficar bonito, mas agora está muito mais claro para o artigo.



Código de arquivo lógico completo

using Microsoft.AspNetCore.SignalR;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using System.Timers;

namespace SnakeApplication.WebApp.Hubs
{
    public class SnakeHub : Hub
    {
    }

    public class SnakeSender
    {
        class Vector2
        {
            public int X { get; set; }
            public int Y { get; set; }
            public Vector2(int x, int y)
            {
                this.X = x;
                this.Y = y;
            }
        }

        class Vector2Comparer : IEqualityComparer<Vector2>
        {
            public bool Equals([AllowNull] Vector2 value1, [AllowNull] Vector2 value2)
            {
                return value1.X == value2.X && value1.Y == value2.Y;
            }

            public int GetHashCode([DisallowNull] Vector2 obj)
            {
                return 0;
            }
        }

        private readonly static Vector2Comparer vector2Comparer = new Vector2Comparer();


        [JsonConverter(typeof(StringEnumConverter))]
        enum Cell
        {
            None,
            Mouse,
            Snake
        }

        enum Directions
        {
            Up,
            Left,
            Down,
            Rigth
        }

        class StateBoard
        {
            public bool IsLife { get; set; }
            public bool IsWin { get; set; }
            public int XSize { get; set; }
            public int YSize { get; set; }
            public Vector2 Mouse { get; set; }
            public List<Vector2> Piton { get; set; }
            public List<Vector2> HamiltonPath { get; set; }
        }

        const int xSize = 12, ySize = 12;
        private Random Rand { get; }
        private IServiceProvider ServiceProvider { get; }
        private bool IsLife { get; set; }
        private bool IsWin { get; set; }
        Directions Direction { get; set; }
        private Vector2 Mouse { get; set; }
        private Queue<Vector2> Piton { get; set; }
        private bool InvertHamiltonPath { get; set; }
        private List<Vector2> HamiltonPath { get; }
        private Queue<Vector2> TempPath { get; set; }
        private int StepsCountAfterCalculatePath { get; set; }

    public SnakeSender(IServiceProvider serviceProvider)
        {
            this.Rand = new Random();
            this.ServiceProvider = serviceProvider;
            this.TempPath = new Queue<Vector2>();
            this.HamiltonPath = new List<Vector2>(xSize * ySize);
            this.CreateHamiltonPath();
            this.CreateBoard();
        }

        private void CreateHamiltonPath()
        {
            this.HamiltonPath.Clear();
            this.HamiltonPath.Add(new Vector2(0, 0));
            HamiltonStep(this.HamiltonPath.Last());
        }

        private bool HamiltonStep(Vector2 current)
        {
            if (HamiltonPath.Count == HamiltonPath.Capacity)
            {
                var first = HamiltonPath.First();
                return (first.X == current.X && first.Y == current.Y - 1)
                    || (first.X == current.X && first.Y == current.Y + 1)
                    || (first.X - 1 == current.X && first.Y == current.Y)
                    || (first.X + 1 == current.X && first.Y == current.Y);
            }
            foreach (var direction in new[] { Directions.Down, Directions.Rigth, Directions.Up, Directions.Left })
            {
                Vector2 newElement = null;
                switch (direction)
                {
                    case Directions.Up:
                        newElement = new Vector2(current.X, current.Y - 1);
                        break;
                    case Directions.Left:
                        newElement = new Vector2(current.X - 1, current.Y);
                        break;
                    case Directions.Down:
                        newElement = new Vector2(current.X, current.Y + 1);
                        break;
                    case Directions.Rigth:
                        newElement = new Vector2(current.X + 1, current.Y);
                        break;
                }
                if (0 <= newElement.X && newElement.X < xSize
                    && 0 <= newElement.Y && newElement.Y < ySize
                    && !HamiltonPath.Contains(newElement, vector2Comparer))
                {
                    HamiltonPath.Add(newElement);
                    if (HamiltonStep(newElement))
                    {
                        return true;
                    }
                    HamiltonPath.Remove(newElement);
                }
            }
            return false;
        }

        private void CreateBoard()
        {
            Task.Run(async () =>
            {
                this.Piton = new Queue<Vector2>();
                //for (int i = 0; i < 1; i++)
                //{
                //    this.Piton.Enqueue(new Vector2(ySize / 2, xSize / 2 - i));
                //}
                this.Piton.Enqueue(new Vector2(0, 0));
                this.IsLife = true;
                this.Direction = Directions.Up;
                this.CreateMouse();
                while (this.IsLife)
                {
                    this.LifeCycle();
                    await Task.Delay(100);
                }
            });
        }

        private void LifeCycle()
        {
            this.SetDirection();
            this.Step();
            this.CheckDead();
            this.Render();
        }

        private void SetDirection()
        {
            Vector2 head = this.Piton.Last();
            int currentIndnex = this.HamiltonPath.FindIndex(p => p.X == head.X && p.Y == head.Y);
            Vector2 currentElement = this.HamiltonPath[currentIndnex];
            Vector2 nextElement = null;
            if (this.TempPath.Count > 0)
            {
                nextElement = this.TempPath.Dequeue();
            }
            else
            {
                this.StepsCountAfterCalculatePath++;
                if (this.InvertHamiltonPath)
                {
                    nextElement = (currentIndnex - 1 < 0) ? this.HamiltonPath[this.HamiltonPath.Count - 1] : this.HamiltonPath[currentIndnex - 1];
                }
                else
                {
                    nextElement = (currentIndnex + 1 == this.HamiltonPath.Count) ? this.HamiltonPath[0] : this.HamiltonPath[currentIndnex + 1];
                }
            }

            if (currentElement.X == nextElement.X && currentElement.Y < nextElement.Y)
            {
                this.Direction = Directions.Down;
                return;
            }

            if (currentElement.X == nextElement.X && nextElement.Y < currentElement.Y)
            {
                this.Direction = Directions.Up;
                return;
            }

            if (currentElement.X < nextElement.X && currentElement.Y == nextElement.Y)
            {
                this.Direction = Directions.Rigth;
                return;
            }

            if (nextElement.X < currentElement.X && currentElement.Y == nextElement.Y)
            {
                this.Direction = Directions.Left;
                return;
            }

            throw new NotImplementedException();
        }

        private void Step()
        {
            Vector2 head = this.Piton.Last();
            switch (this.Direction)
            {
                case Directions.Up:
                    this.Piton.Enqueue(new Vector2(head.X, head.Y - 1));
                    break;
                case Directions.Left:
                    this.Piton.Enqueue(new Vector2(head.X - 1, head.Y));
                    break;
                case Directions.Down:
                    this.Piton.Enqueue(new Vector2(head.X, head.Y + 1));
                    break;
                case Directions.Rigth:
                    this.Piton.Enqueue(new Vector2(head.X + 1, head.Y));
                    break;
            }
            if (this.Piton.Contains(this.Mouse, vector2Comparer))
            {
                CreateMouse(); 
            }
            else
            {
                this.Piton.Dequeue();
            }
            if (this.Piton.Count < this.StepsCountAfterCalculatePath) {
                this.CalculatePath();
            }
        }

        private void CheckDead()
        {
            Vector2 head = this.Piton.Last();
            if (head.X < 0
             || head.Y < 0
             || xSize <= head.X
             || ySize <= head.Y
             || this.Piton.SkipLast(1).Contains(head, vector2Comparer))
            {
                this.IsLife = false;
                this.IsWin = false;
                return;
            }
        }

        private void Render()
        {
            var hubContext = (IHubContext<SnakeHub>)this.ServiceProvider.GetService(typeof(IHubContext<SnakeHub>));
            var piton = this.Piton.ToList();
            piton.Reverse();
            hubContext.Clients?.All.SendAsync("newState", JsonConvert.SerializeObject(new StateBoard()
            {
                IsLife = this.IsLife,
                IsWin = this.IsWin,
                XSize = xSize,
                YSize = ySize,
                Mouse = this.Mouse,
                Piton = piton,
                HamiltonPath = HamiltonPath
            }));
        }

        private void CreateMouse()
        {
            List<Vector2> emptyCells = GetEmptyCells();
            if (emptyCells.Count > 0)
            {
                this.Mouse = emptyCells[this.Rand.Next(emptyCells.Count)];
                this.CalculatePath();
            }
            else
            {
                this.IsLife = false;
                this.IsWin = true;
            }
        }

        private void CalculatePath()
        {
            this.StepsCountAfterCalculatePath = 0;
            int finalIndexPoint = this.HamiltonPath.FindIndex(p => p.X == this.Mouse.X && p.Y == this.Mouse.Y);
            List<Vector2> tempPath = new List<Vector2>();
            List<Vector2> stepPiton = new List<Vector2>(this.Piton);
            Debug.WriteLine($"Piton length: {this.Piton.Count}");
            int index = 0;
            var result = StepTempPath(ref index, GetInvert(stepPiton, this.Mouse), this.Piton.Last(), finalIndexPoint, stepPiton, tempPath);
            if (result.PathIsFound)
            {
                this.TempPath = new Queue<Vector2>(tempPath);
                this.InvertHamiltonPath = result.InvertHamiltonPath;
            }
        }

        private bool GetInvert(List<Vector2> stepPiton, Vector2 finalPoint)
        {
            if (this.Piton.Count > 1)
            {
                int pitonDirection = stepPiton.Last().Y - stepPiton[stepPiton.Count - 2].Y;
                int mouseDirection = stepPiton.Last().Y - finalPoint.Y;
                return (pitonDirection < 0 && mouseDirection < 0) || (pitonDirection > 0 && mouseDirection > 0);
            }
            return false;
        }

        class ResultAnlaizePath
        {
            public bool PathIsFound { get; set; }
            public bool InvertHamiltonPath { get; set; }
            public ResultAnlaizePath(bool pathIsFound, bool invertHamiltonPath = false)
            {
                PathIsFound = pathIsFound;
                InvertHamiltonPath = invertHamiltonPath;
            }
        }

        private ResultAnlaizePath StepTempPath(ref int index, bool invert, Vector2 current, int finalIndexPoint, List<Vector2> stepPiton, List<Vector2> tempPath)
        {
            index++;
            if (this.HamiltonPath.Count < index)
            {
                return new ResultAnlaizePath(false);
            }
            Debug.WriteLine($"index {index} {tempPath.Count}");
            var finalPoint = this.HamiltonPath[finalIndexPoint];
            if (current.X == finalPoint.X && current.Y == finalPoint.Y)
            {
                if (this.Piton.Count == 1)
                {
                    return new ResultAnlaizePath(true);
                }
                foreach (var d in new[] { false, true })
                {
                    var tempPiton = stepPiton.TakeLast(this.Piton.Count).ToList();
                    bool isFound = true;
                    bool invertHamiltonPath = d;
                    for (int j = 1; j < this.Piton.Count; j++)
                    {
                        Vector2 hamiltonPoint;
                        if (invertHamiltonPath)
                        {
                            hamiltonPoint = (finalIndexPoint - j >= 0) ? this.HamiltonPath[finalIndexPoint - j] : this.HamiltonPath[this.HamiltonPath.Count - j];
                        }
                        else
                        {
                            hamiltonPoint = (finalIndexPoint + j < this.HamiltonPath.Count) ? this.HamiltonPath[finalIndexPoint + j] : this.HamiltonPath[finalIndexPoint + j - this.HamiltonPath.Count];
                        }
                        if (tempPiton.TakeLast(this.Piton.Count).Contains(hamiltonPoint, vector2Comparer))
                        {
                            isFound = false;
                            break;
                        }
                        tempPiton.Add(hamiltonPoint);
                    }
                    if (isFound)
                    {
                        return new ResultAnlaizePath(true, invertHamiltonPath);
                    }
                }
                return new ResultAnlaizePath(false);
            }
            if ((xSize + ySize * 2) <= tempPath.Count)
            {
                return new ResultAnlaizePath(false);
            }
            Vector2 newElement = null;

            if (invert)
            {
                if (current.X < finalPoint.X)
                {
                    newElement = new Vector2(current.X + 1, current.Y);
                }
                else if (finalPoint.X < current.X)
                {
                    newElement = new Vector2(current.X - 1, current.Y);
                }
                else if (current.Y < finalPoint.Y)
                {
                    newElement = new Vector2(current.X, current.Y + 1);
                }
                else if (finalPoint.Y < current.Y)
                {
                    newElement = new Vector2(current.X, current.Y - 1);
                }
            }
            else
            {
                if (current.Y < finalPoint.Y)
                {
                    newElement = new Vector2(current.X, current.Y + 1);
                }
                else if (finalPoint.Y < current.Y)
                {
                    newElement = new Vector2(current.X, current.Y - 1);
                }
                else if (current.X < finalPoint.X)
                {
                    newElement = new Vector2(current.X + 1, current.Y);
                }
                else if (finalPoint.X < current.X)
                {
                    newElement = new Vector2(current.X - 1, current.Y);
                }
            }

            if (!stepPiton.TakeLast(this.Piton.Count).Contains(newElement, vector2Comparer))
            {
                tempPath.Add(newElement);
                stepPiton.Add(newElement);
                var retult = StepTempPath(ref index, !invert, newElement, finalIndexPoint, stepPiton, tempPath);
                if (retult.PathIsFound)
                {
                    return retult;
                }
                if (this.HamiltonPath.Count < index)
                {
                    return new ResultAnlaizePath(false);
                }
                tempPath.Remove(newElement);
                stepPiton.Remove(newElement);
            }

            Vector2 nextFinalPoint;
            if (this.InvertHamiltonPath)
            {
                nextFinalPoint = (finalIndexPoint - 1 < 0) ? this.HamiltonPath[this.HamiltonPath.Count - 1] : this.HamiltonPath[finalIndexPoint - 1];
            }
            else
            {
                nextFinalPoint = (finalIndexPoint + 1 == this.HamiltonPath.Count) ? this.HamiltonPath[0] : this.HamiltonPath[finalIndexPoint + 1];
            }
            List<Directions> directions = new List<Directions>(4);
            directions.Add(finalPoint.Y < nextFinalPoint.Y ? Directions.Up : Directions.Down);
            directions.Add(finalPoint.X < nextFinalPoint.X ? Directions.Left : Directions.Rigth);
            directions.Add(finalPoint.Y < nextFinalPoint.Y ? Directions.Down : Directions.Up);
            directions.Add(finalPoint.X < nextFinalPoint.X ? Directions.Rigth : Directions.Left);

            foreach (var direction in directions)
            {
                switch (direction)
                {
                    case Directions.Up:
                        newElement = new Vector2(current.X, current.Y - 1);
                        break;
                    case Directions.Left:
                        newElement = new Vector2(current.X - 1, current.Y);
                        break;
                    case Directions.Down:
                        newElement = new Vector2(current.X, current.Y + 1);
                        break;
                    case Directions.Rigth:
                        newElement = new Vector2(current.X + 1, current.Y);
                        break;
                }
                if (0 <= newElement.X && newElement.X < xSize
                 && 0 <= newElement.Y && newElement.Y < ySize
                 && !stepPiton.TakeLast(this.Piton.Count).Contains(newElement, vector2Comparer))
                {
                    tempPath.Add(newElement);
                    stepPiton.Add(newElement);
                    var retult = StepTempPath(ref index, GetInvert(stepPiton, finalPoint), newElement, finalIndexPoint, stepPiton, tempPath);
                    if (retult.PathIsFound)
                    {
                        return retult;
                    }
                    if (this.HamiltonPath.Count < index)
                    {
                        return new ResultAnlaizePath(false);
                    }
                    tempPath.Remove(newElement);
                    stepPiton.Remove(newElement);
                }
            }
            return new ResultAnlaizePath(false);
        }

        private List<Vector2> GetEmptyCells()
        {
            List<Vector2> emptyCells = new List<Vector2>(xSize * ySize);
            for (int i = 0; i < ySize; i++)
            {
                for (int j = 0; j < xSize; j++)
                {
                    if (!this.Piton.Contains(new Vector2(j, i), vector2Comparer))
                    {
                        emptyCells.Add(new Vector2(j, i));
                    }
                }
            }

            return emptyCells;
        }
    }
}

      
      







Na verdade, como tudo se arrepia.





Obrigado a todos pela atenção.



Agora resta escrever um AI normal para ele.



All Articles