Live Coding Snake

Today's post is about a very common game that we all know from the old Nokia phones.

The idea of this exercise is to see and work with the majority of programming elements and techniques we have been learning throughout the basic programming course.

 

The Snake Game

In this post we’ll see a solution for how we can create this game in .NET.

1 - Analysis

First of all, we will analyze the main requirements of the game, roughly summarized as:

  • We have a board or area where we can move the snake
  • The snake can move
    • Up
    • Down
    • Right
    • Left
  • Candies appear on the screen to eat and score points

So if we turn these real-world entities into code entities, as we saw in the video on objects and classes, we get the following:

  1. Class Board
    • Properties:
      1. Height
      2. Width
    • Methods:
      1. Draw the board
  2. Snake class
    • Properties:
      1. Tail = List of positions
      2. Current direction
      3. Points
    • Methods
      1. Die
      2. Move
      3. Eat the candy
  3. Candy class
    • Appears

In addition to these classes, maybe we need to create some additional ones, like a Util class that draws a line or character on the screen, since this is an action we’ll repeat constantly.

static class Util
{
    public static void DibujarPosicion(int x, int y, string caracter)
    {
        Console.SetCursorPosition(x, y);
        Console.WriteLine(caracter);
    }
}

As we can see, the Console.SetCursorPosition(x,y) method positions the cursor inside the terminal at the coordinates we want.

And as we saw in the video about keyboard and screen input/output, Console.WriteLine(string) prints text. In most cases in this exercise, it’s just a single character. For the walls, it's the # character or x for the snake.

 

 

2 - Code

When programming, we should think as if we had those objects in front of us, so the first element we would set is the board; thus, the Board entity will be the first one we create.

 

2.1 - Board Entity

We create the board class with the properties Height and Width, and, since we’ll use it later, we add the HasCandy property which we initialize to false, since when we first draw the board, the candy does not exist yet.

We also need to create the DrawBoard method, which loops through the height and width to draw the specified board.

public class Tablero
{
    public readonly int Altura;
    public readonly int Anchura;
    public bool ContieneCaramelo;
    public Tablero(int altura, int anchura)
    {
        Altura = altura;
        Anchura = anchura;
        ContieneCaramelo = false;
    }

    public void DibujarTablero()
    {
        for (int i = 0; i <= Altura; i++)
        {
            Util.DibujarPosicion(Anchura, i, "#");
            Util.DibujarPosicion(0, i, "#");
        }

        for (int i = 0; i <= Anchura; i++)
        {
            Util.DibujarPosicion(i, 0, "#");
            Util.DibujarPosicion(i, Altura, "#");
        }
    }
}

 

2.2 - Snake Entity

Of course, it’s very important that we create the snake entity. For this, it's key to remember the previous data. As we said, it will contain a list of positions, so Position itself will be an entity, containing the X and Y axis coordinates.

public class Posicion
{
    public  int X;
    public  int Y;

    public Posicion(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Besides the List<Position> it should contain the direction in which it will move. Since there are only four possible ones and they are always the same, we’ll create them in an enumeration.

public enum Direccion
{
    Arriba,
    Abajo, 
    Izquierda,
    Derecha
}

Using the enumeration makes it much easier to know which direction we're pointing to.

Finally, the cla

public class Serpiente
{
    List<Posicion> Cola { get; set; }
    public Direccion Direccion { get; set; }
    public int Puntos { get; set; }

    public Serpiente(int x, int y)
    {

        Posicion posicionInicial = new Posicion(x, y);
        Cola = new List<Posicion>() { posicionInicial };
        Direccion = Direccion.Abajo;
        Puntos = 0;
    }

    public void DibujarSerpiente()
    {
        foreach (Posicion posicion in Cola)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Util.DibujarPosicion(posicion.X, posicion.Y, "x");
            Console.ResetColor();
        }
    }

    public bool ComprobarMorir(Tablero tablero)
    {
       throw new NotImplementedException();
    }
    

    public void Moverse(bool haComido)
    {
       throw new NotImplementedException();
    }

    public bool ComeCaramelo(Caramelo caramelo, Tablero tablero)
    {
        throw new NotImplementedException();
    }
}

Finally, the Snake class. As we can see, we initialize it with a position and then assign default values to Direction, Points, and the IsAlive property that tells us when it's alive or when the game has ended.

As you can see, I have also created the DrawSnake method, which loops through the tail and prints the snake on the screen using the previously created class. The Console.ForegroundColor code changes the color printed by the console. Meanwhile, Console.ResetColor() restores the default values.

The rest of the methods are not implemented yet. 

 

2.3 - Candy Entity

We can’t forget one of the most important parts of this game, because when we eat a candy, the snake must increase in size. Therefore, we create the Candy entity and draw it. 

public class Caramelo
{
    public Posicion Posicion { get; set; }

    public Caramelo(int x, int y)
    {
        Posicion = new Posicion(x, y);
    }

    public void DibujarCaramelo()
    {
        Console.ForegroundColor = ConsoleColor.Blue;
        Util.DibujarPosicion(Posicion.X, Posicion.Y, "O");
        Console.ResetColor();
    }

    public static Caramelo CrearCaramelo(Serpiente serpiente, Tablero tablero)
    {
        throw new NotImplementedException();
    }
}

 

 

3 - Program Logic

Now that we have the entities defined, even when some methods are not implemented yet, we need to decide how the logic should work so the snake can move and eat candies. 

To do this, we go to the program class, which as you may remember from the first post, is the class executed when you run a console application.

For that, we must print both the board and the snake first. But if we add nothing else, the snake will stay static. As we recall, we have the Direction property that tells us where the snake will move. And for it to move, we need to put it inside a while or do while loop. 

static void Main(string[] args)
{
    Tablero tablero = new Tablero(20, 20);

    Serpiente serpiente = new Serpiente(10, 10);
    Caramelo caramelo = new Caramelo(0, 0);
    bool haComido = false;

    do
    {
        Console.Clear();
        tablero.DibujarTablero();
        serpiente.Moverse(haComido);
        haComido = serpiente.ComeCaramelo(caramelo, tablero);        
        serpiente.DibujarSerpiente();

       

    } while (serpiente.ComprobarMorir(tablero));


    Console.ReadKey();
}

We must initialize the board, snake and first candy outside the loop, because if we placed them inside, we would be starting from scratch every time.

Also, the first line inside the loop should clear the console using Console.Clear() because it is not cleared automatically.

Finally, in this small code snippet, remember that the snake moves first and then eats. It ends by drawing itself.

 

3.1 - The Snake Moves

The first step of the program is moving, so we need to change the position we are in, depending on the direction:

  • Up: Y -1
  • Down: Y+1
  • Right: X +1
  • Left: X-1

We get the first position of the tail (the head) and update its value as you can see in GetNewHeadPosition().

public void Moverse(bool haComido)
{
    List<Posicion> nuevaCola = new List<Posicion>();
    nuevaCola.Add(ObtenerNuevaPrimeraPosicion());
    nuevaCola.AddRange(Cola);

    if (!haComido)
    {
        nuevaCola.Remove(nuevaCola.Last());
    }

    Cola = nuevaCola;
}

private Posicion ObtenerNuevaPrimeraPosicion()
{
    int x = Cola.First().X;
    int y = Cola.First().Y;

    switch (Direccion)
    {
        case Direccion.Abajo:
            y += 1;
            break;
        case Direccion.Arriba:
            y -= 1;
            break;
        case Direccion.Derecha:
            x += 1;
            break;
        case Direccion.Izquierda:
            x -= 1;
            break;
    }
    return new Posicion(x, y);
}

Moving the rest of the snake is simple. The entire tail moves by one unit.

We simply set the new value we've obtained as the first value of a new list. And to that new list, we append the previous tail.

For example, if we have 3 elements:

Position X

Position Y

10

10

10

12

10

12

The next move is up, that is Y -1, the value we get will be:

Position X

Position Y

10

9

Then we concatenate the previous list, so we have the following:

Position X

Position Y

10

9

10

10

10

11

10

12

But this list has one more value than desired. This is where the check for whether the snake just ate from the previous loop comes in: if it did, the list is correct; if not, we must remove the last value from the list.

So we have the following result:

Position X

Position Y

10

9

10

10

10

11

As we can see, these are the new positions for the snake. 

 

3.2 - The Snake Eats

To check if the snake has eaten, we must ensure that the candy is not in any position of the tail. It shouldn’t be possible for the candy to spawn on the snake, but we check all just in case. 

Inside the snake class, the code to check if we have eaten is as follows:

public bool ComeCaramelo(Caramelo caramelo, Tablero tablero)
{
    if (PosicionEnCola(caramelo.Posicion.X, caramelo.Posicion.Y))
    {
        Puntos += 10; // add points
        tablero.ContieneCaramelo = false;//Remove or generate a new candy
        return true;
    }
    return false;

}

public bool PosicionEnCola(int x, int y)
{
    return Cola.Any(a => a.X == x && a.Y == y);
}

ComeCaramelo returns true if we eat the candy, and also tells the board there is no more candy. That will force it to generate a new one. 

 

3.3 - Move in Different Directions

But of course, as it is now, we only move in one direction. We need to allow the user to enter which direction we want to move.

To do that, inside our loop, we must add the following code:

var sw = Stopwatch.StartNew();
while (sw.ElapsedMilliseconds <= 250)
{
    serpiente.Direccion = Util.LeerMovimiento(serpiente.Direccion);
}

The ReadMovement method will return the Direction we have pressed; if we don’t press a new direction within 200 milliseconds, it will automatically return the current direction.

static Direccion LeerMovimiento(Direccion movimientoActual)
{
    if (Console.KeyAvailable)
    {
        var key = Console.ReadKey().Key;

        if (key == ConsoleKey.UpArrow && movimientoActual != Direccion.Abajo)
        {
            return Direccion.Arriba;
        }
        else if (key == ConsoleKey.DownArrow && movimientoActual != Direccion.Arriba)
        {
            return Direccion.Abajo;
        }
        else if (key == ConsoleKey.LeftArrow && movimientoActual != Direccion.Derecha)
        {
            return Direccion.Izquierda;
        }
        else if (key == ConsoleKey.RightArrow && movimientoActual != Direccion.Izquierda)
        {
            return Direccion.Derecha;
        }
    }
    return movimientoActual;
}   

We need to avoid going in the opposite direction to the one we’re currently going, or that will make us crash and die by colliding with ourselves.

 

3.4 - Draw the Candy

Drawing the candy is very simple, we’ll do something similar to when drawing the snake or the walls. The only difference is that to draw the candy, we need to ensure two things.

  • It is totally random
  • It is not on the walls or on the snake.
public void DibujarCaramelo()
{
    Console.ForegroundColor = ConsoleColor.Blue;
    Util.DibujarPosicion(Posicion.X, Posicion.Y, "O");
    Console.ResetColor();
}

public static Caramelo CrearCaramelo(Serpiente serpiente, Tablero tablero)
{
    bool carameloValido;
    int x,y;

    do
    {
        Random random = new Random();
        x = random.Next(1, tablero.Anchura - 1);
        y = random.Next(1, tablero.Altura - 1);
        carameloValido = serpiente.PosicionEnCola(x, y);

    } while (carameloValido);

    tablero.ContieneCaramelo = true;
    return new Caramelo(x, y);
}

 

3.5 - Dying

For the dying part, we will check only two things.

  • The head of the snake (or first position in the list) is on a wall.
  • The head of the snake is on the rest of the snake's tail.
public void ComprobarMorir(Tablero tablero)
{
    //If we crash into ourselves
    Posicion primeraPosicion = Cola.First();

    EstaViva = !((Cola.Count(a => a.X == primeraPosicion.X && a.Y == primeraPosicion.Y) > 1 )
        || CabezaEstaEnPared(tablero, Cola.First()));

}

//if the first position is on any of the walls, we die.
private bool CabezaEstaEnPared(Tablero tablero, Posicion primeraPoisicon)
{
    return primeraPoisicon.Y == 0 || primeraPoisicon.Y == tablero.Altura
        || primeraPoisicon.X == 0 || primeraPoisicon.X == tablero.Anchura;

}

 

3.6 - Put the Pieces Together 

Finally, to make everything work correctly we need to put each part together in a meaningful order. For that, inside the main class, we’ll divide the code as follows.

First, as we’ve seen before, we initialize the needed variables, which include the board, the snake, the candy, and the hasEaten variable.

Tablero tablero = new Tablero(20, 20);

Serpiente serpiente = new Serpiente(10, 10);
Caramelo caramelo = new Caramelo(0, 0);
bool haComido = false;

Once we have everything initialized, we need to draw everything. Remember all this is inside a do While loop. Inside the loop, first of all, we clear the screen, then draw everything, as long as the snake is alive.

We can check if the snake is alive either at the start or end of the loop. It doesn’t matter. But if we check at the start, we must execute the movement, snake creation and candy creation only if we are still alive. Otherwise, we display the message that we have died, along with the score.

So the result code for the main class is as follows

static void Main(string[] args)
{
    Tablero tablero = new Tablero(20, 20);

    Serpiente serpiente = new Serpiente(10, 10);
    Caramelo caramelo = new Caramelo(0, 0);
    bool haComido = false;


    do
    {
        Console.Clear();
        tablero.DibujarTablero();
        //move and check if the snake ate on the previous turn.
        serpiente.Moverse(haComido);

        //Check if the candy was eaten
        haComido = serpiente.ComeCaramelo(caramelo, tablero);

        //Draw snake
        serpiente.DibujarSerpiente();

        //If there is no candy, instantiate a new one.
        if (!tablero.ContieneCaramelo)
        {
            caramelo = Caramelo.CrearCaramelo(serpiente, tablero);
        }

        //Draw candy
        caramelo.DibujarCaramelo();

        //Read direction input from keyboard.
        var sw = Stopwatch.StartNew();
        while (sw.ElapsedMilliseconds <= 250)
        {
            serpiente.Direccion = Util.LeerMovimiento(serpiente.Direccion);
        }

    } while (serpiente.ComprobarMorir(tablero));

    Util.MostrarPuntuación(tablero, serpiente);

    Console.ReadKey();
}

Here’s what the finished exercise looks like: 

juego snake c#

 

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café