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

First instalment

posted in duke_meister
Published February 17, 2019
Advertisement

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


}

 

Next Entry Mostly done
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

1437 views

Changes I made...

1318 views

All done

1932 views

That's it

2116 views

Mostly done

1608 views

First instalment

2213 views
Advertisement