Jump to content
  • Advertisement
Sign in to follow this  
  • entries
    3
  • comment
    1
  • views
    221

That's it

duke_meister

704 views

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

 



1 Comment


Recommended Comments

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
  • Advertisement
  • What is your GameDev Story?

    In 2019 we are celebrating 20 years of GameDev.net! Share your GameDev Story with us.

    (You must login to your GameDev.net account.)

  • Blog Entries

  • Similar Content

    • By John Baxter
      Hey guys. I just released my first game called "Paper Pigeon". Please check it out and give it a good rating if you liked it. This started as just a simple "Flappy Bird" clone to familiarize myself with Unity and just learn programming in general. Midway through the process I started having my own ideas and different takes on this game design which birthed this game. It probably took WAY longer than an experienced programmer could make this in, but nonetheless, I am quite proud to have created this. Please support it fellow gamedevs! Thanks.
      https://play.google.com/store/apps/details?id=com.IndyRiotGames.PaperPigeon

    • By sun113344
      Hi,
      Currently I'm trying to get my ai enemy path find to the player. I've got astar working as expected but i want the enemy ai to jump at an arc when it need to jump between platforms. Here how I'm calculating initail velocity using suvat physics equations:
       
      public LaunchData CalculateLaunchData(Vector3 launchTarget, Vector3 projectilePositions, float height) { float displacementY = launchTarget.y - projectilePositions.y; Vector3 displacementXZ = new Vector3(launchTarget.x - projectilePositions.x, 0, launchTarget.z - projectilePositions.z); float time = Mathf.Sqrt(-2 * height / gravity) + Mathf.Sqrt(2 * (displacementY - height) / gravity); Vector3 velocityY = Vector3.up * Mathf.Sqrt(-2 * gravity * height); Vector3 velocityXZ = displacementXZ / time; return new LaunchData(velocityXZ + velocityY * -Mathf.Sign(gravity), time); }  
      According to the v = u + at formula, I can use time and initail velocity to calculate final. So does final velocity mean velocity at any time t? My idea was todo something like this in my enemy ai class, but it doesnt quite work expected as my enemy is having a weird landing and jumping behaviour and not as smooth as when I just add displacement to original position. Here is what I'm doing:
       
      IEnumerator Jump(LaunchData launchData) { float timer = 0.0f; while (timer <= launchData.timeToTarget) { velocity.x = launchData.initialVelocity.x + (timer); velocity.y = launchData.initialVelocity.y + (timer * (ps.gravity)); timer += Time.deltaTime; enemyController.Move(velocity * Time.deltaTime); yield return null; } jumping = false; } Where enemyController will handle some collision detection related stuff and call Transform Translate method in unity. Something else is that I can use the equation: s = ut + (1/2)(a)(t^2) to get the displacement directly. This works quite well as I can just add the displacement directly to the jump starting position. However, I can only set the transform.position directly with this way and thus I can't handle collisions as nicely as transforming with a velocity (since my resusable code is inside the controller takes velocity). I'm quite new to unity and game programming, so am I doing something wrong with setting my velocity? I can add a little gif later to better visulise things.
    • By Rof
      Hello guys! I am new to Unity3D and also new with C#. I am currently working on a 2D Platformer game. This would be my first game ever. I really need some help in coding and animation. I was able to create script already for my Player Movement. I already have the basics coded and working (Walking, Jumping, Attack, Dash). I also have an extra feature which allows the player to jump longer if he/she presses Space for a longer period of time.

      I just need help in fixing the Jump + Attack. Whenever the player Jumps and Attacks at the same time, the character will ignore the x velocity of the object and will only continue the x velocity after the attack animation.

      I'll really appreciate any help that you can give me 
      Have a great day!
    • By snacktime
      Looking for feedback from someone who has done this, mainly just to confirm that my guestimates are not way off.  I know enough to be dangerous not my area of expertise.
      Trying to budget for a custom 3D skeletal animation system with cross fading and 2 layer blending.  Context is Unity.
      My thought is someone who has done it before could probably get the core features working in some form in a month.  But by the time you factor in everything like bugs, performance refactors, platform specific gotcha's, whatever, it's probably around a 6 month job to get something actually usable in game.
      Does that sound reasonable?
       
       
       
       
       
       
       
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!