Saturday, October 4, 2014

C# Basics: Snake Game in ASCII Art (Part 3)

Hi everyone! :)

Among my early articles were two that showed how to write a simple ASCII art Snake game for the command line in c# (see part 1 and part 2). Recently, someone called Mizukage Yagura requested that I continue this series, specifically to address (a) the Snake moving continuously in a direction until a different arrow key is pressed, and (b) collisions between the Snake and walls or itself. We will address these two points by continuing where we left off in part 2.

Continuous movement

We had already seen in "C# Threading: Bouncing Ball" how we could use a Thread to allow a ball to move continuously while still allowing the command line to wait for user input. This is no different for the Snake. You will first need to add the following statement near the top of your source code:

using System.Threading;

In order to allow the Snake to move in a direction, we need to store that direction. One way to do this is to use an integer, and for instance let 1 mean left, 2 mean right, 3 mean up, and 4 mean down. An even neater way to do this is to declare an enumeration:

    enum Direction { Left, Right, Up, Down };

The values in this enumeration work pretty much like integers, but they are much more readable when you're working with them, as you'll see shortly.

Within the Program class, we can now add a Direction variable, which is where we'll store the current direction in which the Snake is moving:

        static Direction direction;

Just before our game loop (the while (true) bit), we can now set an initial direction (right in this case), and create a thread that will handle the continuous movement. In that thread we are passing a Move() method, which we haven't created yet; but we'll get there soon enough.

            direction = Direction.Right;

            Thread thread = new Thread(Move);
            thread.IsBackground = true;

            thread.Start();

We are now going to make our Snake game work pretty much like "C# Threading: Bouncing Ball": all movement will be handled on our background thread, while keyboard input will be handled in the loop in the main thread. Thus, we can simplify the game loop as follows:

            while (true)
            {
                // handle input

                ConsoleKeyInfo keyInfo = Console.ReadKey(true);

                switch(keyInfo.Key)
                {
                    case ConsoleKey.Escape:
                        return;
                    case ConsoleKey.UpArrow:
                        direction = Direction.Up;
                        break;
                    case ConsoleKey.DownArrow:
                        direction = Direction.Down;
                        break;
                    case ConsoleKey.LeftArrow:
                        direction = Direction.Left;
                        break;
                    case ConsoleKey.RightArrow:
                        direction = Direction.Right;
                        break;
                }

            }

We can now create our Move() method. This mostly contains the old code that took care of moving the Snake, but we put it in its own game loop, add a delay at the end to wait for a short period of time before making the next move, and actually compute the next head location based on the current direction, which is this bit:

                var next = snake[0];

                switch(direction)
                {
                    case Direction.Left:
                        if (next.X > 0)
                            next.X--;
                    break;
                    case Direction.Right:
                        if (next.X < 79)
                            next.X++;
                    break;
                    case Direction.Up:
                        if (next.Y > 0)
                            next.Y--;
                    break;
                    case Direction.Down:
                        if (next.Y < 24)
                            next.Y++;
                    break;

                }

To cut a long story short, once you've arranged things a little bit, your code should look something like this:

using System;
using System.Collections.Generic;
using System.Threading;

namespace CsSnake
{
    struct Location
    {
        public int X;
        public int Y;

        public Location(int x, int y)
        {
            this.X = x;
            this.Y = y;
        }
    };

    enum Direction { Left, Right, Up, Down };

    class Program
    {
        static Direction direction;
        static List<Location> snake = new List<Location>();
        static Location star = new Location(60, 20);

        public static void Main(string[] args)
        {
            Console.OutputEncoding = System.Text.Encoding.GetEncoding(1252);
            Console.Title = "Snake Ranch";

            Location head = new Location(40, 12);
            snake.Add(head);

            direction = Direction.Right;

            Thread thread = new Thread(Move);
            thread.IsBackground = true;
            thread.Start();

            while (true)
            {
                // handle input

                ConsoleKeyInfo keyInfo = Console.ReadKey(true);

                switch(keyInfo.Key)
                {
                    case ConsoleKey.Escape:
                        return;
                    case ConsoleKey.UpArrow:
                        direction = Direction.Up;
                        break;
                    case ConsoleKey.DownArrow:
                        direction = Direction.Down;
                        break;
                    case ConsoleKey.LeftArrow:
                        direction = Direction.Left;
                        break;
                    case ConsoleKey.RightArrow:
                        direction = Direction.Right;
                        break;
                }
            }
        }

        public static void Move()
        {
            while (true)
            {
                var next = snake[0];

                switch(direction)
                {
                    case Direction.Left:
                        if (next.X > 0)
                            next.X--;
                        break;
                    case Direction.Right:
                        if (next.X < 79)
                            next.X++;
                        break;
                    case Direction.Up:
                        if (next.Y > 0)
                            next.Y--;
                        break;
                    case Direction.Down:
                        if (next.Y < 24)
                            next.Y++;
                        break;
                }

                Console.Clear();

                // show snake

                foreach (Location location in snake)
                {
                    Console.SetCursorPosition(location.X, location.Y);
                    Console.ForegroundColor = ConsoleColor.White;
                    Console.Write((char) 178);
                    Console.ResetColor();
                }

                // show star

                Console.SetCursorPosition(star.X, star.Y);
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write('*');
                Console.ResetColor();

                snake.Insert(0, next);
                if (next.X == star.X && next.Y == star.Y)
                {
                    Random random = new Random();
                    star.X = random.Next(0, 80);
                    star.Y = random.Next(0, 25);
                }
                else
                    snake.RemoveAt(snake.Count - 1);

                Thread.Sleep(300);
            }
        }
    }
}


Handling collisions

Now, we want something to happen when the Snake bumps into a wall or itself.

Since we are storing the location of each part of the Snake in a list, it is easy to just check whether the new head has bumped into any of those locations. First, let's change the while (true) loop in the Move() method so that it can exit when we change a flag:

            bool quit = false;
            while (!quit)

At the end of the loop that actually shows the snake, we can now add the following collision-checking logic:

            if (next.X == location.X && next.Y == location.Y)
            {
                quit = true;
                break;

            }

This will effectively make the game stop when your Snake hits itself:


Now, in order to collide with walls, we need to actually store those walls somewhere. Since the command line window is effectively an 80x25 grid, it makes sense to store them in a data structure that resembles a grid. A 2D array is a pretty good choice for this kind of thing. But in this case we can simplify things further by using an array of strings, which effectively works out to be a 2D array of characters. We can thus declare our grid within the Program class:

        static string[] grid = new string[]
        {
            "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
            "X                                                                              X",
            "X    X                                                                    X    X",
            "X    X                                                                    X    X",
            "X    X      XXXXXXXXXXXX                                 XXXXXXXXXXXX     X    X",
            "X    X                                                                    X    X",
            "X    X                                                                    X    X",
            "X    X                                                                    X    X",
            "X    X                                                                    X    X",
            "X    X                           XXXXXXXXXXXXXXXX                         X    X",
            "X    X                           X              X                         X    X",
            "X    X                                   X                                X    X",
            "X    X                                  XXX                               X    X",
            "X    X                                  XXX                               X    X",
            "X    X                                   X                                X    X",
            "X    X                           X              X                         X    X",
            "X    X                           XXXXXXXXXXXXXXXX                         X    X",
            "X    X                                                                    X    X",
            "X    X                                                                    X    X",
            "X    X                                                                    X    X",
            "X    X      XXXXXXXXXXXX                                 XXXXXXXXXXXX     X    X",
            "X    X                                                                    X    X",
            "X    X                                                                    X    X",
            "X                                                                              X",
            "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        };

Now immediately after Console.Clear(), we can draw our grid, which is just a matter of writing the strings in the array:

            Console.Clear();
            for (int i = 0; i < grid.Length; i++)
            {
                Console.Write(grid[i]);

            }

Note that since the walls in the grid never change, this is pretty wasteful since we can just draw it once at the beginning and then only draw the Snake at every move, taking care to clear the square where the snake's tail was; however let's keep it simple.

We can now add an additional condition to our collision detection logic to cater for wall collisions:

                    if (next.X == location.X && next.Y == location.Y
                        || grid[next.X][next.Y] == 'X')
                    {
                        quit = true;
                        break;

                    }

Finally, let's adjust the Snake's starting location so that it isn't at a wall location:

            Location head = new Location(50, 12);

...and do the same for the star:

        static Location star = new Location(60, 15);

...and now we have something that handles collisions:


This is still far from complete, and one of the issues you'll notice from the above screenshot is that there is nothing to prevent the star from appearing over a wall. However, the basic collision detection mechanism is there and it's working - it's up to you to continue developing the game and refine it into a complete product. :)