using System;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace GdiSnake
{
public sealed partial class Form1 : Form
{
// our unchanging values:
// playfield height & width
const int PlayfieldWidth = 80;
const int PlayfieldHeight = 40;
const int CellSize = 9;
const int PlayfieldYOffset = 50;
const int PlayfieldXOffset = 20;
// game pieces
// no longer required
//const string EmptyCell = " ";
//const string SnakeHeadCell = "@";
//const string SnakeBodyCell = "o";
//const string FoodCell = ".";
// timeout to adjust speed of snake
const int MillisecondsTimeout = 30;
// our playfield; stores FieldVals instead of ints so we don't have to remember them
readonly FieldVals[,] PlayField = new FieldVals[PlayfieldWidth, PlayfieldHeight];
// not yet used until we increase length of snake
int _snakeBodyLen; // not including head
// which direction (SnakdDirs enum) the snake is currently moving
SnakeDirs _snakeDir;
// position of the one-and-only piece of food; use our own coordinate class, Pos
readonly Pos FoodPos = new Pos(0, 0);
readonly Pos EraserPos = new Pos(0, 0);
// defines the snake; each element tells us which coordinates each snake piece is at
static int _maxSnakeLen = 31;
Pos[] _snakeCells = new Pos[_maxSnakeLen];
// guess
int _score = 0;
// for randomizing things like food placement
Random _rnd;
// how many body pieces the snake will increase by when it eats food
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
public enum FieldVals { DontDraw, Empty, SnakeHead, SnakeBody, SnakeFood, Border }
// these make it easy (for the human) to read snake the direction
public enum SnakeDirs { Up, Right, Down, Left }
public Form1()
{
DoubleBuffered = true;
InitializeComponent();
RunGame();
}
public void Timer1_Tick(object sender, EventArgs e)
{
CheckForKeyboardCommand();
// using a timer now
//AdjustGameSpeed();
// done in paint() now
//UpdatePlayfield();
CheckForSnakeOutOfBounds();
CheckForSnakeCollisionWithSelf();
UpdateSnakeBodyPosition();
CheckSnakeHasEatenFood();
Invalidate();
}
public 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>
public 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);
// have simplified the snake placement
//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 set 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 = PlayfieldWidth / 2;
_snakeCells.First().Y = PlayfieldHeight / 2;
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;
}
}
// not required for GDI version
//public 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>
public 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>
public 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();
}
}
public void IncreaseSnakeSize()
{
if (_snakeBodyLen + SnakeSizeIncrease < _maxSnakeLen)
{
_snakeBodyLen += SnakeSizeIncrease;
}
}
public void DrawScore(Graphics g)
{
WriteAt(g, $"Score: {_score} Snake Size: {_snakeBodyLen}", 0, 0);
}
public void IncrementScore()
{
++_score;
}
/// <summary>
/// Put food item at random location
/// </summary>
public 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;
}
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>
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>
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();
}
public void RunGame()
{
_rnd = new Random();
//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();
timer1.Start();
// 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();
// }
}
/// <summary>
/// Set the console size appropriately & draw the border, leaving room for the score
/// </summary>
void DrawBorder( Graphics g)
{
for (var i = -1; i <= PlayfieldWidth; i++)
{
DrawCell(g, FieldVals.Border, i, -1);
DrawCell(g, FieldVals.Border, i, PlayfieldHeight);
}
for (var i = -1; i <= PlayfieldHeight; i++)
{
DrawCell(g, FieldVals.Border, -1, i);
DrawCell(g, FieldVals.Border, PlayfieldWidth, 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>
void UpdatePlayfield( Graphics g)
{
DrawBorder( g);
for (var i = 1; i < PlayfieldWidth - 1; i++)
{
for (var j = 1; j < PlayfieldHeight - 1; j++)
{
switch (PlayField[i, j])
{
case FieldVals.Empty:
DrawCell(g, FieldVals.Empty, i, j + 1);
break;
case FieldVals.SnakeHead:
DrawCell( g, FieldVals.SnakeHead, i, j + 1);
break;
case FieldVals.SnakeBody:
DrawCell(g, FieldVals.SnakeBody, i, j + 1);
break;
case FieldVals.SnakeFood:
DrawCell(g, FieldVals.SnakeFood, i, j + 1);
break;
}
}
}
PlayField[EraserPos.X, EraserPos.Y] = FieldVals.DontDraw;
}
public void DrawCell( Graphics g ,FieldVals cellType, int x, int y)
{
Color color = Color.Blue;
switch (cellType)
{
case FieldVals.DontDraw:
break;
case FieldVals.Empty:
break;
case FieldVals.SnakeHead:
color = Color.Blue;
break;
case FieldVals.SnakeBody:
color = Color.DarkCyan;
break;
case FieldVals.SnakeFood:
color = Color.Coral;
break;
case FieldVals.Border:
color = Color.Black;
break;
}
g.FillRectangle( new SolidBrush(color), x * CellSize + PlayfieldXOffset, y * CellSize + PlayfieldYOffset, CellSize, CellSize);
}
// From Microsoft sample
private void WriteAt( Graphics g, string s, int x, int y)
{
using (var drawFont = new Font("Arial", 16))
using (var drawBrush = new SolidBrush(System.Drawing.Color.Black))
{
g.DrawString(s, drawFont, drawBrush, x, y);
}
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
UpdatePlayfield( e.Graphics);
DrawScore( e.Graphics);
}
}
/// <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);
}
}