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

All done

posted in duke_meister
Published February 19, 2019
Advertisement

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

 

Next Entry Changes I made...
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

1439 views

Changes I made...

1319 views

All done

1940 views

That's it

2125 views

Mostly done

1611 views

First instalment

2217 views
Advertisement