miércoles, 1 de junio de 2016

Juego Snake en aplicación de consola (y II). La serpiente.

Códigos de muestra - Ejemplos MSDN. Juego Snake en aplicación de consola

En el artículo anterior, Juego Snake en aplicación de consola (I). Definiendo la pantalla, vimos cómo crear la pantalla inicial del juego Snake, en éste veremos cómo crear la serpiente, dotarla de movimiento y mostrar comida que pueda ir comiendo y aumentando la puntuación y su longitud.

De esta forma completaremos la funcionalidad básica del juego.

Para dibujar la serpiente y dotarla de movimiento voy a definir 3 funciones.

La función GetDirection recibe como argumento la dirección en la que se mueve actualmente la serpiente, comprueba si el jugador ha pulsado alguna de las teclas de dirección y, si es así, devuelve la dirección en la que se debería mover ahora la serpiente.

private static Direction GetDirection(Direction currentDirection)
{
    if (!Console.KeyAvailable) return currentDirection;

    var key = Console.ReadKey(true).Key;
    switch (key)
    {
        case ConsoleKey.DownArrow:
            if (currentDirection != Direction.Up)
                currentDirection = Direction.Down;
            break;
        case ConsoleKey.LeftArrow:
            if (currentDirection != Direction.Right)
                currentDirection = Direction.Left;
            break;
        case ConsoleKey.RightArrow:
            if (currentDirection != Direction.Left)
                currentDirection = Direction.Right;
            break;
        case ConsoleKey.UpArrow:
            if (currentDirection != Direction.Down)
                currentDirection = Direction.Up;
            break;
    }
    return currentDirection;
}
Private Function GetDirection(currentDirection As Direction) As Direction
    If Not Console.KeyAvailable Then Return currentDirection

    Dim key = Console.ReadKey(True).Key
    Select Case key
        Case ConsoleKey.DownArrow
            If currentDirection <> Direction.Up Then _
                currentDirection = Direction.Down
        Case ConsoleKey.LeftArrow
            If currentDirection <> Direction.Rigth Then _
                currentDirection = Direction.Left
        Case ConsoleKey.RightArrow
            If currentDirection <> Direction.Left Then _
                currentDirection = Direction.Rigth
        Case ConsoleKey.UpArrow
            If currentDirection <> Direction.Down Then _
                currentDirection = Direction.Up
    End Select
    Return currentDirection
End Function

La función GetNextPosition recibe como argumentos la posición actual y la dirección en la que debe moverse la serpiente y devuelve la nueva posición de ésta.

private static Point GetNextPosition(Direction direction, Point currentPosition)
{
    Point nextPosition = new Point(currentPosition.X, currentPosition.Y);
    switch (direction)
    {
        case Direction.Up:
            nextPosition.Y--;
            break;
        case Direction.Left:
            nextPosition.X--;
            break;
        case Direction.Down:
            nextPosition.Y++;
            break;
        case Direction.Right:
            nextPosition.X++;
            break;
    }
    return nextPosition;
}
Private Function GetNextPosition(direction As Direction, currentPosition As Point)
    Dim nextPosition As New Point(currentPosition.X, currentPosition.Y)
    Select Case direction
        Case Direction.Up
            nextPosition.Y -= 1
        Case Direction.Left
            nextPosition.X -= 1
        Case Direction.Down
            nextPosition.Y += 1
        Case Direction.Rigth
            nextPosition.X += 1
    End Select
    Return nextPosition
End Function

Por último, la función MoveSnake recibirá como argumentos la lista de posiciones de la serpiente, la posición destino, la longitud de la serpiente y el tamaño de la pantalla, y se encargará de realizar el movimiento de la serpiente a la ubicación destino.

Esta función devuelve un valor booleano indicando si el movimiento ha sido posible. Si devuelve false indicará que el movimiento no se ha podido realizar (bien porque ha chocado contra su cola o ha salido de los límites de la pantalla) y por lo tanto debería finalizar el juego.

La función comprueba si la posición destino coincide con alguna de las posiciones ya incluidas en la cola de la serpiente o si está fuera de los límites de la pantalla y, si es así, devuelve false.

Si no es así toma la anterior posición de la cabeza y escribe un espacio con fondo verde, que es el color que utilizaremos para la cola. Esta posición, como era hasta este movimiento la cabeza de la serpiente, debería estar pintado con color rojo, que es el color que utilizaremos para pintar la cabeza de la serpiente.

A continuación añade la nueva posición de la cabeza a la cola de la serpiente y la dibuja con color rojo (establecemos el fondo rojo y escribimos un espacio).

Por último comprueba si la longitud de la serpiente supera la longitud máxima y, si es así, extrae la última posición de la cola de la serpiente y la elimina de la pantalla (escribiendo un espacio con fondo negro).

private static bool MoveSnake(Queue<Point> snake, Point targetPosition, 
    int snakeLength, Size screenSize)
{
    var lastPoint = snake.Last();

    if (lastPoint.Equals(targetPosition)) return true;

    if (snake.Any(x => x.Equals(targetPosition))) return false;

    if (targetPosition.X < 0 || targetPosition.X >= screenSize.Width
            || targetPosition.Y < 0 || targetPosition.Y >= screenSize.Height)
    {
        return false;
    }

    Console.BackgroundColor = ConsoleColor.Green;
    Console.SetCursorPosition(lastPoint.X + 1, lastPoint.Y + 1);
    Console.WriteLine(" ");

    snake.Enqueue(targetPosition);

    Console.BackgroundColor = ConsoleColor.Red;
    Console.SetCursorPosition(targetPosition.X + 1, targetPosition.Y + 1);
    Console.Write(" ");

    // Quitar cola
    if (snake.Count > snakeLength)
    {
        var removePoint = snake.Dequeue();
        Console.BackgroundColor = ConsoleColor.Black;
        Console.SetCursorPosition(removePoint.X + 1, removePoint.Y + 1);
        Console.Write(" ");
    }
    return true;
}
Private Function MoveSnake(snake As Queue(Of Point), targetPosition As Point, _
                           snakeLength As Integer, screenSize As Size)
    Dim lastPoint = snake.Last()

    If lastPoint.Equals(targetPosition) Then Return True

    If snake.Any(Function(x) x.Equals(targetPosition)) Then Return False

    If targetPosition.X < 0 OrElse targetPosition.X >= screenSize.Width _
        OrElse targetPosition.Y < 0 OrElse targetPosition.Y >= screenSize.Height Then
        Return False
    End If

    Console.BackgroundColor = ConsoleColor.Green
    Console.SetCursorPosition(lastPoint.X + 1, lastPoint.Y + 1)
    Console.WriteLine(" ")

    snake.Enqueue(targetPosition)

    Console.BackgroundColor = ConsoleColor.Red
    Console.SetCursorPosition(targetPosition.X + 1, targetPosition.Y + 1)
    Console.Write(" ")

    ' Quitar cola
    If snake.Count > snakeLength Then
        Dim removePoint = snake.Dequeue()
        Console.BackgroundColor = ConsoleColor.Black
        Console.SetCursorPosition(removePoint.X + 1, removePoint.Y + 1)
        Console.Write(" ")
    End If
    Return True
End Function


Teniendo estas tres funciones simplemente tendremos que declarar un bucle para ir modificando la dirección, calculando la nueva posición y realizando el correspondiente movimiento hasta que un movimiento no se pueda realizar. Momento en el que finalizará el juego mostrando un mensaje de "Game Over".

static void Main()
{
    var score = 0;
    var speed = 100;
    var foodPosition = Point.Empty;
    var screenSize = new Size(60, 20);
    var snake = new Queue<Point>();
    var snakeLength = 3;
    var currentPosition = new Point(0, 9);
    snake.Enqueue(currentPosition);
    var direction = Direction.Right;

    DrawScreen(screenSize);
    ShowScore(score);

    while (MoveSnake(snake, currentPosition, snakeLength, screenSize))
    {
        Thread.Sleep(speed);
        direction = GetDirection(direction);
        currentPosition = GetNextPosition(direction, currentPosition);
    }

    Console.ResetColor();
    Console.SetCursorPosition(screenSize.Width/2 - 4, screenSize.Height/2);
    Console.Write("Game Over");
    Thread.Sleep(2000);
    Console.ReadKey();
}
Sub Main()
    Dim score = 0
    Dim speed = 100
    Dim foodPosition = Point.Empty
    Dim screenSize As New Size(60, 20)
    Dim snake As New Queue(Of Point)
    Dim snakeLength = 3
    Dim currentPosition As New Point(0, 9)
    snake.Enqueue(currentPosition)
    Dim direction As Direction = Direction.Rigth

    DrawScreen(screenSize)
    ShowScore(score)

    While MoveSnake(snake, currentPosition, snakeLength, screenSize)
        Thread.Sleep(speed)
        direction = GetDirection(direction)
        currentPosition = GetNextPosition(direction, currentPosition)
    End While

    Console.ResetColor()
    Console.SetCursorPosition(screenSize.Width/2 - 4, screenSize.Height/2)
    Console.Write("Game Over")
    Thread.Sleep(2000)
    Console.ReadKey()
End Sub

Ahora, si arrancamos la aplicación podremos ver que ya aparece la serpiente y somos capaces de dirigir sus movimientos por la pantalla utilizando las flechas del teclado.




Añadir la comida y puntuación

Nos queda ya un único detalle para completar el juego: mostrar elementos que se pueda "comer" la serpiente de forma que el jugador pueda ir acumulando puntos y, al mismo tiempo, aumentar la longitud de la serpiente para aumentar la dificultad.

Para este último paso definiremos una nueva función ShowFood que recibirá como argumentos el tamaño de la pantalla y las posiciones que ocupa la serpiente. La función calcula de forma aleatoria un punto en el que mostrar la comida de la serpiente, comprobando que el punto no esté ocupado por la serpiente y que tenga una distancia mínima con la cabeza de la serpiente.

Una vez calculado el punto dibuja una espacio azul que representará la comida y devuelve la ubicación al método principal.

private static Point ShowFood(Size screenSize, Queue<Point> snake)
{
    var foodPoint = Point.Empty;
    var snakeHead = snake.Last();
    var rnd = new Random();
    do
    {
        var x = rnd.Next(0, screenSize.Width - 1);
        var y = rnd.Next(0, screenSize.Height - 1);
        if (snake.All(p => p.X != x || p.Y != y)
            && Math.Abs(x - snakeHead.X) + Math.Abs(y - snakeHead.Y) > 8)
        {
            foodPoint = new Point(x, y);
        }

    } while (foodPoint == Point.Empty);

    Console.BackgroundColor = ConsoleColor.Blue;
    Console.SetCursorPosition(foodPoint.X + 1, foodPoint.Y + 1);
    Console.Write(" ");

    return foodPoint;
}
Private Function ShowFood(screenSize As Size, snake As Queue(Of Point)) As Point
    Dim foodPoint = Point.Empty
    Dim snakeHead = snake.Last()
    Dim rnd As New Random()
    Do
        Dim x = rnd.Next(0, screenSize.Width - 1)
        Dim y = rnd.Next(0, screenSize.Height - 1)
        If snake.All(Function(p) p.X <> x OrElse p.Y <> y) _
           AndAlso Math.Abs(x - snakeHead.X) + Math.Abs(y - snakeHead.Y) > 8 Then
            foodPoint = new Point(x, y)
        End If
    Loop While foodPoint = Point.Empty

    Console.BackgroundColor = ConsoleColor.Blue
    Console.SetCursorPosition(foodPoint.X + 1, foodPoint.Y + 1)
    Console.Write(" ")

    Return foodPoint
End Function

Ya sólo nos queda modificar el método principal para que vaya mostrando la comida de la serpiente con la función ShowFood. En cada movimiento la aplicación comprobará si la cabeza de la serpiente se ha movido a la ubicación en que se encontraba la comida y, de ser así, aumenta la puntuación y la longitud de la serpiente y muestra un nuevo elemento de comida.

static void Main()
{
    var score = 0;
    var speed = 100;
    var foodPosition = Point.Empty;
    var screenSize = new Size(60, 20);
    var snake = new Queue<Point>();
    var snakeLength = 3;
    var currentPosition = new Point(0, 9);
    snake.Enqueue(currentPosition);
    var direction = Direction.Right;

    DrawScreen(screenSize);
    ShowScore(score);

    while (MoveSnake(snake, currentPosition, snakeLength, screenSize))
    {
        Thread.Sleep(speed);
        direction = GetDirection(direction);
        currentPosition = GetNextPosition(direction, currentPosition);

        if (currentPosition.Equals(foodPosition))
        {
            foodPosition = Point.Empty;
            snakeLength++;
            score += 10;
            ShowScore(score);
        }

        if (foodPosition == Point.Empty)
        {
            foodPosition = ShowFood(screenSize, snake);
        }
    }

    Console.ResetColor();
    Console.SetCursorPosition(screenSize.Width/2 - 4, screenSize.Height/2);
    Console.Write("Game Over");
    Thread.Sleep(2000);
    Console.ReadKey();
}
Sub Main()
    Dim score = 0
    Dim speed = 100
    Dim foodPosition = Point.Empty
    Dim screenSize As New Size(60, 20)
    Dim snake As New Queue(Of Point)
    Dim snakeLength = 3
    Dim currentPosition As New Point(0, 9)
    snake.Enqueue(currentPosition)
    Dim direction As Direction = Direction.Rigth

    DrawScreen(screenSize)
    ShowScore(score)

    While MoveSnake(snake, currentPosition, snakeLength, screenSize)
        Thread.Sleep(speed)
        direction = GetDirection(direction)
        currentPosition = GetNextPosition(direction, currentPosition)

        If currentPosition.Equals(foodPosition) Then
            foodPosition = Point.Empty
            snakeLength += 1
            score += 10
            ShowScore(score)
        End If

        If foodPosition = Point.Empty Then
            foodPosition = ShowFood(screenSize, snake)
        End If
    End While

    Console.ResetColor()
    Console.SetCursorPosition(screenSize.Width/2 - 4, screenSize.Height/2)
    Console.Write("Game Over")
    Thread.Sleep(2000)
    Console.ReadKey()
End Sub

Y de esta forma tendríamos ya la operativa principal del juego funcionando:

Juego snake finalizado

A partir de aquí se podría añadir otras funciones como paredes que tenga que evitar la serpiente, diferentes diseños de pantallas, diferentes niveles de dificultad modificando la velocidad o la posibilidad de guardar las puntuaciones logradas.

Pero estas cosas ya las dejo para cada uno. A mí me apetece ponerme a jugar un par de partidas.


El código completo tanto en C# como en Visual Basic .NET está disponible en:

Códigos de muestra - Ejemplos MSDN. Juego Snake en aplicación de consola

No hay comentarios:

Publicar un comentario