Gigi Labs

Please follow Gigi Labs for the latest articles. Programmer's Ranch no longer has its domain, so please update your bookmarks and links to programmersranch.blogspot.com.

Wednesday, May 15, 2013

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

Hi all! :)

In yesterday's article, C# Basics: Snake Game in ASCII Art (Part 1), we created the first part of our Snake Ranch game (a clone of Snake) and learned to use structs. Today we will learn to use lists in order to allow the snake to grow.

At the beginning of yesterday's article, we mentioned that we needed to store each part of the snake in a list, as follows:


The head of the snake would be at (1,1), for example. The remaining parts trail behind it in sequence. We can store these locations in a list, in the following fashion:


There is a List collection in .NET that allows us to do this kind of thing. As with dictionaries (see "C# Basics: Morse Code Converter Using Dictionaries", we first need to include System.Collections.Generic first:

using System.Collections.Generic;

We can now declare a new List of Location objects, and put the head as the first item:

            List<Location> snake = new List<Location>();
            snake.Add(head);

We now change the code that shows the head (from yesterday's article) to show the entire snake in the list:

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

All we're doing here is going over each element in the list and showing a portion of the snake there. For now we only have the head though.

Before we make the snake grow, we first need to understand what happens when it moves. Consider this again:


Let's say the head of the snake is at (1,1), and it moves to the left. Then the head becomes (0,1). The tail (last part) of the snake goes away, and all the parts between the new head and the old tail remain unchanged. We need to make some changes to our code to handle this properly. First, before the while loop, we declare a new Location variable called next:

            Location next;

This will hold the position of the new head, based on the keypress received. In the example above, the head is currently at (1,1), but if the user presses the left arrow, then we want next to contain (0,1).

At the beginning of the while loop, we add the following:

                next = snake[0];
               
                Console.Clear();

The first line assigns next to the location of the current head, stored in the first element of the snake variable. Lists can be accessed using [] notation just like arrays.

Next, we change the input handling logic to modify next instead of head:

                switch(keyInfo.Key)
                {
                    case ConsoleKey.Escape:
                        return;
                    case ConsoleKey.UpArrow:
                        if (next.Y > 0)
                            next.Y--;
                        break;
                    case ConsoleKey.DownArrow:
                        if (next.Y < 24)
                            next.Y++;
                        break;
                    case ConsoleKey.LeftArrow:
                        if (next.X > 0)
                            next.X--;
                        break;
                    case ConsoleKey.RightArrow:
                        if (next.X < 79)
                            next.X++;
                        break;
                }

After that, we can do the following:

                snake.Insert(0, next);
                snake.RemoveAt(snake.Count - 1);

Here we are doing exactly what I explained earlier: adding a new head at the beginning of the snake, and removing the old tail. If you press F5 now, you should have functionality just like what we had at the end of yesterday's article (because we still have just one part of the snake):


All we have left to do now is to make the snake get longer when he eats a star. At the beginning, add a new variable to store the location of the star:

Location star = new Location(6020);

After showing the snake, add the following code to show the star:

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

Finally, replace the last two lines in the while loop (Insert() and RemoveAt()) with the following logic:

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

This code is based on the code from Chase the Star (see "C#: ASCII Art Game (Part 2)"). If the snake bumps into the star, the star moves away to some other random location. Here we are using structs instead of separate starX and starY variables.

The most important thing to understand here is that if the player bumps into the star, we don't remove the snake's tail, thus allowing it to grow. We can now test it:



Awesome! :) We have just made our own variant of Snake using ASCII art. If you test it thoroughly, you'll notice there are a few problems:

  1. The snake doesn't move on its own as time goes by. You need to press an arrow key for it to move.
  2. The snake can bump into itself or the edges and nothing bad happens.
  3. If the snake moves into the edge, it sort of compresses. When you move away, it regains its former length.
  4. The star can appear over the snake.
  5. No obstacles... boring!

As you can see, it can be quite complicated to make a complete game, as there are many things you need to take care of. Making a fully blown Snake game would take many more articles, so I'll leave it at that. In this article we use generic lists to implement the Snake concept. If you're really adventurous, you might want to try and fixing some of the issues above as an exercise. Don't worry if you don't manage - just thinking about strategies to solve them will teach you how to think like a programmer.

I hope you enjoyed this, and come back for more programming articles! :)

Here is the full code for Snake Ranch:

using System;
using System.Collections.Generic;

namespace CsSnake
{
    struct Location
    {
        public int X;
        public int Y;
       
        public Location(int x, int y)
        {
            this.X = x;
            this.Y = y;
        }
    };
   
    class Program
    {
        public static void Main(string[] args)
        {
            Console.OutputEncoding = System.Text.Encoding.GetEncoding(1252);
            Console.Title = "Snake Ranch";
           
            Location head = new Location(4012);
            List<Location> snake = new List<Location>();
            snake.Add(head);
           
            Location next;

            Location star = new Location(6020);
           
            while (true)
            {
                next = snake[0];
               
                Console.Clear();
               
                // show snake
               
                foreach (Location location in snake)
                {
                    Console.SetCursorPosition(location.X, location.Y);
                    Console.ForegroundColor = ConsoleColor.White;
                    Console.Write((char178);
                    Console.ResetColor();
                }
               
                // show star
               
                Console.SetCursorPosition(star.X, star.Y);
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write('*');
                Console.ResetColor();
               
                // handle input
               
                ConsoleKeyInfo keyInfo = Console.ReadKey(true);
               
                switch(keyInfo.Key)
                {
                    case ConsoleKey.Escape:
                        return;
                    case ConsoleKey.UpArrow:
                        if (next.Y > 0)
                            next.Y--;
                        break;
                    case ConsoleKey.DownArrow:
                        if (next.Y < 24)
                            next.Y++;
                        break;
                    case ConsoleKey.LeftArrow:
                        if (next.X > 0)
                            next.X--;
                        break;
                    case ConsoleKey.RightArrow:
                        if (next.X < 79)
                            next.X++;
                        break;
                }
               
                snake.Insert(0, next);
                if (next.X == star.X && next.Y == star.Y)
                {
                    Random random = new Random();
                    star.X = random.Next(080);
                    star.Y = random.Next(025);
                }
                else
                    snake.RemoveAt(snake.Count - 1);
            }
        }
    }
}

3 comments:

  1. I have some questions!

    What if i want to do walls, and if snake hits the wall, game restarts?
    Also i want to make the snake move automatically if i press like leftarrow key, then it moves left until i press something else like uparrow key.
    Also how i can tell the snake that if it touches itself, then die and restart game again?

    ReplyDelete
    Replies
    1. Hi there! If you want to take care of collisions, then you need to store everything that appears on the 'map'. We're already storing the snake itself, but for walls you might want to use a 2D array. Then, for each move, you can check whether a wall or the snake itself is on that square, and restart the game accordingly.

      Letting the snake continue to move independently is slightly more complicated but I have covered that concept in another article - See "C# Threading: Bouncing Ball".

      Obviously this article is oversimplified because I was trying to introduce some simple concepts without letting it get complicated. Making part 3 is an interesting suggestion as I could use it to introduce multidimensional arrays and basic collision detection. I'll do it if I find some time over the weekend.

      Delete