🎉 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!

Mostly done

posted in duke_meister
Published February 17, 2019
Advertisement

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;

        // 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];

        // 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;

        // 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 = 20;
        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();

            Console.Clear();

            // 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 */;)
            {
                AdjustGameSpeed();
                CheckForKeyboardCommand();
                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()
        {
            // 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.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()
        {
            var cellsDrawn = 0;

            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;
                    }
                    ++cellsDrawn;
                }
            }

            Debug.Assert( cellsDrawn <= _snakeBodyLen + 3);
//            Debug.WriteLine($"Cells drawn: {cellsDrawn}");

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


}

Can't seem to put text above the code. Anyway, this is playable except you sometimes die immediately. Need to tweak the snake placement code.

Metro Exodus has downloaded, so off to play that :D

Have fun :)

Previous Entry First instalment
Next Entry That's it
0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement

Latest Entries

Bit of a cleanup

1440 views

Changes I made...

1319 views

All done

1940 views

That's it

2125 views

Mostly done

1612 views

First instalment

2217 views
Advertisement