Still some rough edges and lots more that could be done, but it's essentially finished. You can restart a game, see the score and the snake can get much longer (a trivial change).
This hasn't really been a blog, but somewhere to drop this code.
Maybe I'll make another one that goes through it step by step (because although console is obviously not for gaming, a lot of the basics can be learned from a project like this).
Another idea would be to make a blog about turning this exact code into a graphics (e.g. GDI) version with a minimum of changes... That would be geared more towards beginner C# programmers.
If anyone's interested
using System;
using System.Linq;
using System.Threading.Tasks;
namespace ConsoleSnake
{
/// <summary>
/// All code written by duke_meister (Valentino Rossi)
/// except keyboard reading technique
/// </summary>
class Program
{
// our unchanging values:
// playfield height & width
const int PlayfieldWidth = 80;
const int PlayfieldHeight = 40;
// game pieces
const string EmptyCell = " ";
const string SnakeHeadCell = "@";
const string SnakeBodyCell = "o";
const string FoodCell = ".";
// timeout to adjust speed of snake
static int MillisecondsTimeout = 50;
// our playfield; stores FieldVals instead of ints so we don't have to remember them
static readonly FieldVals[,] PlayField = new FieldVals[PlayfieldWidth, PlayfieldHeight];
static int _snakeBodyLen; // not including head
// which direction (SnakdDirs enum) the snake is currently moving
static SnakeDirs _snakeDir;
// position of the one-and-only piece of food; use our own coordinate class, Pos
static readonly Pos FoodPos = new Pos(0, 0);
static readonly Pos EraserPos = new Pos(0, 0);
// defines the snake; each element tells us which coordinates each snake piece is at
static int _maxSnakeLen = 30;
static Pos[] _snakeCells = new Pos[_maxSnakeLen];
// guess
static int _score = 0;
// for randomizing things like food placement
static Random _rnd;
// how many body pieces the snake will increase by when it eats food
static int SnakeSizeIncrease = 2;
// could've used something existing, but made a simple screen coordinate class
public class Pos
{
public int X { get; set; }
public int Y { get; set; }
public Pos(int x, int y)
{
X = x;
Y = y;
}
}
// these make it easy (for the human) to know what each cell contains
enum FieldVals { DontDraw, Empty, SnakeHead, SnakeBody, SnakeFood }
// these make it easy (for the human) to read snake the direction
enum SnakeDirs { Up, Right, Down, Left }
static void Main(string[] args)
{
_rnd = new Random();
RunGame();
}
private static void RunGame()
{
Console.Clear();
_score = 0;
for (var i = 0; i < PlayfieldWidth; i++)
{
for (var j = 0; j < PlayfieldHeight; j++)
{
PlayField[i, j] = FieldVals.DontDraw;
}
}
// create the initial snake cell coords (place it on playfield)
SetUpSnake();
// start with an initial piece of food
MakeNewFood();
// draw the border, once
DrawBorder();
// game loop; this was the easiest but might switch to Timer, etc.
// function names should explain purpose
for (;/* ever */; )
{
CheckForKeyboardCommand();
AdjustGameSpeed();
UpdatePlayfield();
CheckForSnakeOutOfBounds();
CheckForSnakeCollisionWithSelf();
UpdateSnakeBodyPosition();
CheckSnakeHasEatenFood();
}
}
private static void CheckForSnakeCollisionWithSelf()
{
if( _snakeCells.Skip(1).Any(pos => pos.X == _snakeCells.First().X && pos.Y == _snakeCells.First().Y))
{
EndGame(false);
}
}
/// <summary>
/// Work out the initial coordinates of the snake's body parts
/// </summary>
private static void SetUpSnake()
{
_snakeBodyLen = 4;
// create the empty snake array cells
for (var i = 0; i < _snakeCells.Length; i++)
{
_snakeCells[i] = new Pos(0, 0);
}
// randomly choose snake's initial direction
_snakeDir = (SnakeDirs)_rnd.Next((int)SnakeDirs.Up, (int)SnakeDirs.Left + 1);
int[] xOffsets = { 0, _snakeBodyLen * -1, 0, _snakeBodyLen};
int[] yOffsets = { _snakeBodyLen, 0, _snakeBodyLen * -1, 0};
int xOffset = xOffsets[(int) _snakeDir];
int yOffset = yOffsets[(int) _snakeDir];
// First randomly choose the position of the snake's head
// We'll work out the rest of the snake body coords based on which
// direction it's initially facing.
_snakeCells.First().X = _rnd.Next( xOffset * _snakeBodyLen * -1, PlayfieldWidth + xOffset * _snakeBodyLen + 1);
_snakeCells.First().Y = _rnd.Next( yOffset * _snakeBodyLen * -1, PlayfieldHeight + yOffset * _snakeBodyLen + 1);
switch (_snakeDir)
{
case SnakeDirs.Up:
// make the snake's body go below the head, as it's moving up
for (int i = 1; i < _snakeBodyLen; i++)
{
_snakeCells[i].X = _snakeCells.First().X;
_snakeCells[i].Y = _snakeCells[i - 1].Y + 1;
}
break;
case SnakeDirs.Right:
// make the snake's body go left of the head, as it's moving right
for (int i = 1; i < _snakeBodyLen; i++)
{
_snakeCells[i].X = _snakeCells.First().X - 1;
_snakeCells[i].Y = _snakeCells.First().Y;
}
break;
case SnakeDirs.Down:
// make the snake's body go above of the head, as it's moving down
for (int i = 1; i < _snakeBodyLen; i++)
{
_snakeCells[i].X = _snakeCells.First().X;
_snakeCells[i].Y = _snakeCells[i - 1].Y - 1;
}
break;
case SnakeDirs.Left:
// make the snake's body go right of the head, as it's moving left
for (int i = 1; i < _snakeBodyLen; i++)
{
_snakeCells[i].X = _snakeCells.First().X + 1;
_snakeCells[i].Y = _snakeCells.First().Y;
}
break;
}
}
private static void AdjustGameSpeed()
{
// delay so the game isn't too fast. Halve the delay (to go faster) when going left or right
// as it appears that going up/down is faster
Task.Delay( _snakeDir == SnakeDirs.Up || _snakeDir == SnakeDirs.Right ? MillisecondsTimeout / 2 : MillisecondsTimeout).Wait();
}
/// <summary>
/// Check the keyboard for arrow keys
/// I got the code off the net (see bottom of code); no point re-creating this
/// </summary>
private static void CheckForKeyboardCommand()
{
if (NativeKeyboard.IsKeyDown(KeyCode.Down)) // player hit Down arrow
{
// can't hit down while going up; game over
if (_snakeDir == SnakeDirs.Up)
EndGame(false);
// change snake direction to down
_snakeDir = SnakeDirs.Down;
}
else if (NativeKeyboard.IsKeyDown(KeyCode.Up))
{
// can't hit up while going down; game over
if (_snakeDir == SnakeDirs.Down)
EndGame(false);
// change snake direction to up
_snakeDir = SnakeDirs.Up;
}
else if (NativeKeyboard.IsKeyDown(KeyCode.Left))
{
// can't hit left while going right; game over
if (_snakeDir == SnakeDirs.Right)
EndGame(false);
// change snake direction to left
_snakeDir = SnakeDirs.Left;
}
else if (NativeKeyboard.IsKeyDown(KeyCode.Right))
{
// can't hit right while going left; game over
if (_snakeDir == SnakeDirs.Left)
EndGame(false);
// change snake direction to right
_snakeDir = SnakeDirs.Right;
}
}
/// <summary>
/// See if snake has eaten the food
/// </summary>
private static void CheckSnakeHasEatenFood()
{
// if snake head is in the same x,y position as the food
// NB: First() is a Linq function; it gives me the first element in the array
if (_snakeCells.First().X == FoodPos.X && _snakeCells.First().Y == FoodPos.Y)
{
IncrementScore();
MakeNewFood();
IncreaseSnakeSize();
}
}
private static void IncreaseSnakeSize()
{
if (_snakeBodyLen + SnakeSizeIncrease <= _maxSnakeLen)
{
_snakeBodyLen += SnakeSizeIncrease;
UpdateScore();
}
}
private static void UpdateScore()
{
WriteAt($"Score: {_score} Snake Size: {_snakeBodyLen}", 0, 0);
}
private static void IncrementScore()
{
++_score;
UpdateScore();
}
/// <summary>
/// Put food item at random location
/// </summary>
private static void MakeNewFood()
{
int x, y;
do
{
// this ensures we're not putting the food on top of the snake, or the border
x = _rnd.Next(1, PlayfieldWidth - 1);
y = _rnd.Next(1, PlayfieldHeight - 1);
} while (_snakeCells.Any(pos => pos.X == x || pos.Y == y));
// set the food coords
FoodPos.X = x;
FoodPos.Y = y;
// update the playfield position with the food value
PlayField[FoodPos.X, FoodPos.Y] = FieldVals.SnakeFood;
}
static void CheckForSnakeOutOfBounds()
{
// snake mustn't be on any border cell, or game over
if (_snakeCells.First().Y < 1 || _snakeCells.First().X > PlayfieldWidth - 2 ||
_snakeCells.First().Y > PlayfieldHeight - 2 || _snakeCells.First().X < 1)
{
EndGame(false);
}
}
/// <summary>
/// Move the snake pieces appropriately. I just did the simplest thing that I thought of.
/// </summary>
static void UpdateSnakeBodyPosition()
{
// remember the position of the snake's last piece so that later,
// after drawing the snake, we can set it to the 'don't draw' value
EraserPos.X = _snakeCells[_snakeBodyLen].X;
EraserPos.Y = _snakeCells[_snakeBodyLen].Y;
// Last piece of snake's tail will always become empty as the snake moves
// NB: Last() is a Linq function; it gives me the last element in the array (end of snake tail)
PlayField[_snakeCells[_snakeBodyLen].X, _snakeCells[_snakeBodyLen].Y] = FieldVals.Empty;
// move the 'middle' section of the snake one cell along
for (int i = _snakeCells.Length - 1; i > 0; i--)
{
_snakeCells[i].X = _snakeCells[i - 1].X;
_snakeCells[i].Y = _snakeCells[i - 1].Y;
}
// move the snake's head, depending on direction moving
// the body was already moved above
switch (_snakeDir)
{
case SnakeDirs.Up:
// moved the snake head up 1 (-ve Y direction)
--_snakeCells.First().Y;
break;
case SnakeDirs.Right:
// moved the snake head right 1 (+ve X direction)
++_snakeCells.First().X;
break;
case SnakeDirs.Down:
// moved the snake head up 1 (+ve Y direction)
++_snakeCells.First().Y;
break;
case SnakeDirs.Left:
// moved the snake head left 1 (-ve X direction)
--_snakeCells.First().X;
break;
}
// Set the playfield position at the head of the snake, to be... the snake head!
PlayField[_snakeCells.First().X, _snakeCells.First().Y] = FieldVals.SnakeHead;
// Set the positions on the playfield for the snake body cells
// so we know to draw them
// NB: Skip(1).Take(4) is Linq; it gives me the array left after
// skipping the first item, then grabbing the next 4 (so in this
// case misses the first and last).
foreach (var cell in _snakeCells.Skip(1).Take(4))
{
PlayField[cell.X, cell.Y] = FieldVals.SnakeBody;
}
}
/// <summary>
/// Just show a message and exit (can only lose right now)
/// </summary>
/// <param name="win"></param>
static void EndGame(bool win)
{
Console.Clear();
Console.WriteLine($"YOU DIED. Score: {_score} Snake Length: {_snakeBodyLen}");
Console.ReadKey();
Console.WriteLine("P to play again, Q to quit.");
var consoleKeyInfo = Console.ReadKey();
if (consoleKeyInfo.Key == ConsoleKey.Q)
{
Environment.Exit(0);
}
RunGame();
}
/// <summary>
/// Set the console size appropriately & draw the border, leaving room for the score
/// </summary>
static void DrawBorder()
{
Console.SetWindowSize(PlayfieldWidth, PlayfieldHeight + 2);
WriteAt("╔", 0, 1);
WriteAt("╗", PlayfieldWidth - 1, 1);
WriteAt("╚", 0, PlayfieldHeight);
WriteAt("╝", PlayfieldWidth - 1, PlayfieldHeight);
for (var i = 1; i < PlayfieldWidth - 1; i++)
{
WriteAt("═", i, 1);
WriteAt("═", i, PlayfieldHeight);
}
for (var i = 2; i < PlayfieldHeight; i++)
{
WriteAt("║", 0, i);
WriteAt("║", PlayfieldWidth - 1, i);
}
}
/// <summary>
/// Go through every element of the 2d array, only drawing a cell
/// if it has a value (other than 0). This way we only draw the
/// cells that need to be updated. A bit like Invalidate() in GDO.
/// Pretty self-explanatory; if a cell has a value, draw the character
/// appropriate for it. The space is only used to overwrite the last
/// piece of the snake's tail.
/// </summary>
static void UpdatePlayfield()
{
for (var i = 1; i < PlayfieldWidth - 1; i++)
{
for (var j = 1; j < PlayfieldHeight - 1; j++)
{
switch (PlayField[i, j])
{
case FieldVals.Empty:
WriteAt( EmptyCell, i, j + 1);
break;
case FieldVals.SnakeHead:
WriteAt(SnakeHeadCell, i, j + 1);
break;
case FieldVals.SnakeBody:
WriteAt(SnakeBodyCell, i, j + 1);
break;
case FieldVals.SnakeFood:
WriteAt(FoodCell, i, j + 1);
PlayField[FoodPos.X, FoodPos.Y] = FieldVals.DontDraw;
break;
}
}
}
PlayField[EraserPos.X, EraserPos.Y] = FieldVals.DontDraw;
}
// From Microsoft sample
protected static void WriteAt(string s, int x, int y)
{
try
{
Console.SetCursorPosition(x, y);
Console.Write(s);
}
catch (ArgumentOutOfRangeException e)
{
Console.Clear();
Console.WriteLine(e.Message);
}
}
}
/// <summary>
/// Codes representing keyboard keys.
/// </summary>
/// <remarks>
/// Key code documentation:
/// http://msdn.microsoft.com/en-us/library/dd375731%28v=VS.85%29.aspx
/// </remarks>
internal enum KeyCode
{
Left = 0x25,
Up,
Right,
Down
}
/// <summary>
/// Provides keyboard access.
/// </summary>
internal static class NativeKeyboard
{
/// <summary>
/// A positional bit flag indicating the part of a key state denoting
/// key pressed.
/// </summary>
const int KeyPressed = 0x8000;
/// <summary>
/// Returns a value indicating if a given key is pressed.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns>
/// <c>true</c> if the key is pressed, otherwise <c>false</c>.
/// </returns>
public static bool IsKeyDown(KeyCode key)
{
return (GetKeyState((int)key) & KeyPressed) != 0;
}
/// <summary>
/// Gets the key state of a key.
/// </summary>
/// <param name="key">Virtual-key code for key.</param>
/// <returns>The state of the key.</returns>
[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern short GetKeyState(int key);
}
}
Nice one!