Serpiente, Ratón y Hamilton

Buen día. Enseñemos a la computadora a jugar a la serpiente.



En realidad, esta es la primera parte de un artículo sobre cómo puede intentar solucionar este problema. Digamos simplemente calentamiento, antes de la pelea principal.





Cómo empezó todo



Para ser completamente franco, todo comenzó viendo un video de un tipo de Australia que se hace llamar " Code Bullet ".

Está tratando de resolver un simple juego de serpientes usando varios algoritmos de inteligencia artificial.

Realmente no tiene éxito, y por eso ... Los algoritmos actuales que proporciona la comunidad de IA ahora resuelven solo dos problemas, ya sea Clasificación o Regresión (predicción). Pero la serpiente no cabe ni allí ni allí. Por la idea de que comerse un ratón es bueno y chocar es malo. Se rompe en la cola, que crece y crece constantemente hasta ocupar todo el campo. Así que me pareció una buena idea escribir una IA de autoaprendizaje en toda regla para tal tarea, pero primero decidí calentar y escribir solo un algoritmo que resolvería el problema de frente, sin entrenamiento. En realidad, se discutirá este algoritmo.



PD El artículo no contendrá grandes descubrimientos, sino ideas "tomadas" de otros y su propia implementación con una historia sobre lo que vino y de dónde.



Escribir un juego



Antes de jugar, escribamos el juego en sí.



Todos los cálculos se realizarán en el servidor, la serpiente se mostrará en el navegador y la información se intercambiará a través de WebSocket (SignalR).



Código aburrido sin el que 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;
        }
}
      
      







Al comienzo del juego, tenemos un ratón rojo y una serpiente unicelular.



Y ahora de alguna manera tenemos que empezar a jugar.



En general, jugar a la serpiente es muy simple: solo necesita pasar por cada celda de la matriz una vez. Y todo el problema está resuelto: Happy End.



Para ser más precisos, nuestro campo es un "Gráfico conectado". Aquellos. cada celda del tablero es un vértice. Los bordes van desde cada vértice; se trata de una transición a un vértice adyacente.

Hay de 2 a 4 de estos bordes, para la celda extrema y para la central, respectivamente.



En algún lugar del 4 de agosto de 1805 al 2 de septiembre de 1865, un tal Hamilton William Rowan que vivía en Irlanda, investigó el problema de "viajar alrededor del mundo" a lo largo del dodecaedro. En este problema, los vértices del dodecaedro simbolizaban ciudades famosas como Bruselas, Amsterdam, Edimburgo, Beijing, Praga, Delhi, Frankfurt, etc., y los bordes simbolizaban las carreteras que las conectaban. El viajero debe dar la vuelta al mundo, encontrando un camino que atraviese todos los picos exactamente una vez. Para hacer la tarea más interesante, se estableció de antemano el orden de paso de las ciudades. Y para que sea más fácil recordar qué ciudades ya estaban conectadas, se clavó un clavo en cada vértice del dodecaedro y se marcó el camino pavimentado con una pequeña cuerda que se podía enrollar alrededor del clavo. Sin embargo, esta construcción resultó ser demasiado engorrosa y Hamilton propuso una nueva versión del juego, reemplazando el dodecaedro con un gráfico plano,isomorfo al gráfico construido en los bordes del dodecaedro.



En general, existe una broma como "ciclo hamiltoniano". Un ciclo hamiltoniano "es un ciclo (camino cerrado) que pasa por cada vértice de un gráfico dado exactamente una vez; es decir, un ciclo simple que incluye todos los vértices del gráfico. También está estrechamente relacionada con un gráfico hamiltoniano la noción de camino hamiltoniano, que es un camino simple (camino sin bucles) que pasa por cada vértice del gráfico exactamente una vez. Una ruta hamiltoniana se diferencia de un ciclo en que los puntos de inicio y finalización de la ruta pueden no coincidir, a diferencia de un ciclo. El ciclo hamiltoniano es el camino hamiltoniano.



Visualmente, esto se puede representar como





En nuestro caso, entonces.





Solo aquí hay un matiz ... Si intentamos construir un ciclo de este tipo desde la excavadora, obtendremos una enumeración de tantas opciones que será posible esperar hasta la segunda venida.



La conclusión es que el enfoque general para encontrar el "ciclo hamiltoniano" implica una búsqueda exhaustiva y no parece haber nada más óptimo. Y tenemos, con una matriz de 12 por 12, solo 144 vértices y para cada uno necesitamos verificar de 2 a 4 aristas. En general, existe en alguna parte la complejidad de n! ..



Pero como resolvemos un problema para una matriz en la que cada vértice está conectado a todos los vecinos, simplemente puede hacer un pase en el sentido de las agujas del reloj.



Entonces no es difícil construir un "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;
        }
      
      





Y sí, "tomé prestada" esta idea de " Code Bullet ", y la obtuvo de otro tipo en Internet.



En resumen, como dijo Pablo Picasso:

Buenos artistas copian grandes artistas roban






Y así, obtenemos una serpiente que va por un ciclo hasta la victoria:





En general, ¡el problema está resuelto! Pero qué lamentable se ve cuando una serpiente unicelular se arrastra de un punto al otro lado. Aunque no hay obstáculos para tomarlo ...



Y desde el punto de vista de las matemáticas, obtenemos la complejidad en (O) n ^ 2. Donde n es el número de puntos (en nuestro caso n = 144).



Porque cada vez ... cada ... Tenemos que dar la vuelta a todo en un bucle ... En



pocas palabras, nos cansaremos de esperar ... Entonces, aunque la solución es buena, hay un problema: lleva mucho tiempo ...



Mejoramiento



¿Qué sabemos de la serpiente? Su longitud cambia cuando se come el ratón. Y la trayectoria ideal para ella es el camino de Hamelton. Y la distancia más corta entre dos puntos es una línea recta.



Comencemos llamando a la recursividad:



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;
            }
        }
      
      





Pero analicemos la parte recursiva por separado.



La parte principal es lo más sencilla posible.



Nos acercamos obstinadamente a la meta:



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);
                }
      
      





La serpiente no sabe gatear en diagonal, pero ahora primero nos alineamos verticalmente, y luego vamos a la meta horizontalmente. Y luego puede ir a una nueva iteración para verificar.



La comprobación de estado final se parece a esto para empezar.



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;
            }
      
      





¿Qué estamos haciendo realmente aquí?



Cuando encontramos nuestro mouse en la ruta virtual. Además, continuamos construyendo un camino, pero ya por el camino ideal de Hamilton, y si no se cruza con el cuerpo de nuestra serpiente, entonces sabemos con certeza que a lo largo de este camino hacia el mouse actual, puede dejarlo con seguridad. la serpiente se va, ya que después de "comernos un ratón", donde sea que esté el siguiente, podemos recorrer el camino de un ciclo completo y comernos el siguiente.



Mientras seamos unicelulares, no puede haber ningún problema, en principio, pero esto no dura mucho ... Tan pronto como nuestra longitud se convierte en más de un camino directo hacia la meta, se crea un problema. El ciclo tiene una cierta dirección, es decir la serpiente siempre se mueve en el sentido de las agujas del reloj, ya que en realidad nos cuesta el camino.



Y aquí es donde surge el problema. Digamos que llegamos al siguiente mouse desde arriba y dejamos que Hamilton suba a este lugar en particular del camino. La serpiente se moverá contra sí misma, lo que en principio es imposible ... Para solucionar este problema, introduciremos el concepto de una trayectoria de Hamilton invertida.



Aquellos. la serpiente puede moverse no solo en el sentido de las agujas del reloj a lo largo del camino que se construyó, sino también en la dirección opuesta a lo largo de este camino. El camino no cambiará de esto, pero la dirección del movimiento - sí.



Cambiemos el 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);
            }
      
      





Y por cierto, el mencionado "Code Bullet" no pensó en esta finta con los oídos. Estoy hablando de invertir, pero "cortó las esquinas", según algunos de sus algoritmos, que permanecieron como un secreto cubierto de oscuridad. Pero puedo decir con certeza que hubo una articulación en su decisión, por lo que su paso falló.







Bueno, en general, qué más puedo decir. Está claro que además de oponerse al movimiento de la serpiente, puede meterse cursi en su cola. Para solucionar esta situación, escribiremos una búsqueda simple para otra opción de ruta.



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);
      
      





En general, nada complicado.



Solo podemos detenernos en esto.



Aquí estoy tratando de dejar la búsqueda en la dirección opuesta al "movimiento hacia adelante" del mouse descrito anteriormente.



            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);

      
      





Aquellos. "Dar un paso al otro lado" para sortear el obstáculo que se encontró en el camino recto.



A continuación se muestra el código completo, pero podría haberse dividido en archivos para hacerlo de manera hermosa, pero ahora es más claro para el artículo.



Código de archivo 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;
        }
    }
}

      
      







En realidad, cómo se arrastra todo.





Gracias a todos por su atención.



Ahora queda escribir una IA normal para ello.



All Articles