Gigi Labs

Saturday, October 4, 2014

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:

enumDirection
{ 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:

staticDirection
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 = newThread(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)

{

caseConsoleKey.Escape:

return;

caseConsoleKey.UpArrow:

direction
= Direction.Up;

break;

caseConsoleKey.DownArrow:

direction
= Direction.Down;

break;

caseConsoleKey.LeftArrow:

direction
= Direction.Left;

break;

caseConsoleKey.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)

{

caseDirection.Left:

if
(next.X > 0)

next.X--;

break;

caseDirection.Right:

if
(next.X < 79)

next.X++;

break;

caseDirection.Up:

if
(next.Y > 0)

next.Y--;

break;

caseDirection.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

{

structLocation

{

publicint
X;

publicint
Y;

public
Location(int
x, int
y)

{

this.X
= x;

this.Y
= y;

}

};

enumDirection
{ Left, Right, Up, Down };

classProgram

{

staticDirection
direction;

staticList<Location>
snake = newList<Location>();

staticLocation
star = newLocation(60,
20);

publicstaticvoid
Main(string[]
args)

{

Console.OutputEncoding
= System.Text.Encoding.GetEncoding(1252);

Console.Title
= "Snake
Ranch";

Location
head = newLocation(40,
12);

snake.Add(head);

direction
= Direction.Right;

Thread
thread = newThread(Move);

thread.IsBackground
= true;

thread.Start();

while
(true)

{

//
handle input

ConsoleKeyInfo
keyInfo = Console.ReadKey(true);

switch(keyInfo.Key)

{

caseConsoleKey.Escape:

return;

caseConsoleKey.UpArrow:

direction
= Direction.Up;

break;

caseConsoleKey.DownArrow:

direction
= Direction.Down;

break;

caseConsoleKey.LeftArrow:

direction
= Direction.Left;

break;

caseConsoleKey.RightArrow:

direction
= Direction.Right;

break;

}

}

}

publicstaticvoid
Move()

{

while
(true)

{

var
next = snake[0];

switch(direction)

{

caseDirection.Left:

if
(next.X > 0)

next.X--;

break;

caseDirection.Right:

if
(next.X < 79)

next.X++;

break;

caseDirection.Up:

if
(next.Y > 0)

next.Y--;

break;

caseDirection.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 = newRandom();

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:

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 = newLocation(50,
12);

...and do the same for the star:

staticLocation
star = newLocation(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. :)