This code is similar to that I posted in @phil67rpg 's blog but with comments and updated to remove some unnecessary stuff.
I'll finish it off by making the snake grow longer, otherwise it's not much of a challenge.
Be kind on the code, I wanted to see what I could do before bed last night
PS: there's a big bug in the code..
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
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;
// timeout so game isn't too fast
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];
// not yet used until we increase length of snake
static int _snakeBodyLen = 4; // not including head
// which direction (SnakdDirs enum) the snake is currently moving
static SnakeDirs _snakeDir = SnakeDirs.Right;
// position of the one-and-only piece of food; use our own coordinate class, Pos
static readonly Pos FoodPos = new Pos(0, 0);
// defines the snake; each element tells us which coordinates each snake piece is at
static readonly Pos[] SnakeCells = { new Pos(14, 10), new Pos(13, 10), new Pos(12, 10), new Pos(11, 10), new Pos(10, 10) };
// guess
private static int _score = 0;
// for randomizing things like food placement
private static Random _rnd;
// 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 { Empty = 1, 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();
Console.Clear();
// start with empty playfield
for (var i = 0; i < PlayfieldWidth; i++)
{
for (var j = 0; j < PlayfieldHeight; j++)
{
PlayField[i, j] = 0;
}
}
// 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 */;)
{
AdjustGameSpeed();
CheckForKeyboardCommand();
UpdatePlayfield();
CheckForSnakeOutOfBounds();
UpdateSnakeBodyPosition();
CheckSnakeHasEatenFood();
}
}
private static void AdjustGameSpeed()
{
// delay so the game isn't too fast. Halve the delay (to go faster) when going left or right
// as the playfield isn't square
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();
}
}
private static void IncrementScore()
{
++_score;
WriteAt( $"Score: {_score}", 0, 0);
}
/// <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 (plus the update 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()
{
// 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.Last().X, SnakeCells.Last().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 {( win ? "WIN" : "LOSE")}");
Console.ReadKey();
Environment.Exit(0);
}
/// <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( " ", i, j + 1);
break;
case FieldVals.SnakeHead:
WriteAt("@", i, j + 1);
break;
case FieldVals.SnakeBody:
WriteAt("o", i, j + 1);
break;
case FieldVals.SnakeFood:
WriteAt(".", i, j + 1);
break;
}
}
}
}
// 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);
}
}