🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

That's it

posted in duke_meister
Published February 18, 2019
Advertisement

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 :D

 


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);
    }
}

 

Previous Entry Mostly done
0 likes 1 comments

Comments

jbadams

Nice one! 

February 18, 2019 12:13 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement

Latest Entries

Bit of a cleanup

1437 views

Changes I made...

1318 views

All done

1932 views

That's it

2117 views

Mostly done

1608 views

First instalment

2213 views
Advertisement