0

Can someone explain me the logic part? I kinda know it should work but I cant trace the code step by step, It doesnt make sense. Exchange among temp, pre, and Tail part is so confusing.

How does it run with the framerate? Is TailX[0] and TailY[0] always ahead? WHY? How do new tail parts get assigned in correct position? HELP ME.

using System;
using System.Threading;

namespace Projects
{

class Snake
{
    public Random rand = new Random();
    public ConsoleKeyInfo keypress = new ConsoleKeyInfo();

    int score, headX, headY, fruitX, fruitY, nTail;

    int[] TailX = new int[100];
    int[] TailY = new int[100];

    const int height = 20;
    const int width = 60;

    bool gameOver, reset, isprinted, horizontal, vertical;

    string dir, pre_dir;

    void ShowBanner()
    {
        Console.SetWindowSize(width, height + 6);
        Console.ForegroundColor = ConsoleColor.Green;
        Console.CursorVisible = false;

        Console.WriteLine("!!####################################!!");
        Console.WriteLine("!!####################################!!");
        Console.WriteLine("!!####################################!!");
        Console.WriteLine("!!####################################!!");
        Console.WriteLine("!!####################################!!");
        Console.WriteLine("!!##########     Welcome    ##########!!");

        keypress = Console.ReadKey(true);
        if (keypress.Key == ConsoleKey.Escape)
        {
            Environment.Exit(0);
        }
    }

    void Setup()
    {
        dir = "Right";
        pre_dir = "";
        score = 0;
        nTail = 0;

        gameOver = false;
        reset = false;
        isprinted = false;

        headX = width / 2;
        headY = height / 2;
        fruitX = rand.Next(1, width - 1);
        fruitY = rand.Next(1, height - 1);
    }

    void CheckInput()
    {

        while (Console.KeyAvailable)
        {
            keypress = Console.ReadKey(true);
            if (keypress.Key == ConsoleKey.Escape)
                Environment.Exit(0);

            if (keypress.Key == ConsoleKey.S)
            {
                pre_dir = dir;
                dir = "STOP";
            }

            else if (keypress.Key ==ConsoleKey.LeftArrow)
            {
                pre_dir = dir;
                dir = "LEFT";
            }

            else if (keypress.Key == ConsoleKey.RightArrow)
            {
                pre_dir = dir;
                dir = "RIGHT";
            }

            else if (keypress.Key ==ConsoleKey.UpArrow)
            {
                pre_dir = dir;
                dir = "UP";
            }


            else if (keypress.Key == ConsoleKey.DownArrow)
            {
                pre_dir = dir;
                dir = "DOWN";
            }
        }
    }

    void Logic()
    {
        int preX = TailX[0];
        int preY = TailY[0];

        int tempX, tempY;

        if (dir != "STOP")
        {
            TailX[0] = headX;
            TailY[0] = headY;

            for (int i = 1; i < nTail; i++)
            {
                tempX = TailX[i];
                tempY = TailY[i];
                TailX[i] = preX;
                TailY[i] = preY;
                preX = tempX;
                preY = tempY;
            }
        }

        switch (dir)
        {
            case "RIGHT":
                headX++;
                break;

            case "LEFT":
                headX--;
                break;

            case "UP":
                headY--;
                break;

            case "DOWN":
                headY++;
                break;

            case "STOP":

                while (true)
                {
                    Console.Clear();
                    Console.CursorLeft = width / 2 - 6;
                    Console.WriteLine("GAME PAUSED");
                    Console.WriteLine();
                    Console.WriteLine();
                    Console.WriteLine("   -S to resume   ");
                    Console.WriteLine("   -R to reset   ");
                    Console.Write("   -ESC to quit   ");

                    keypress = Console.ReadKey(true);

                    if (keypress.Key == ConsoleKey.Escape)
                        Environment.Exit(0);
                    if (keypress.Key == ConsoleKey.R)
                    {
                        reset = true;
                        break;
                    }

                    if (keypress.Key == ConsoleKey.S)
                        break;
                }

                dir = pre_dir;
                break;
        }

        if (headX <= 0 || headX >= width - 1 || headY <= 0 || headY >= height - 1)
        {
            gameOver = true;
        }
        else
        {
            gameOver = false;
        }

        if (headX == fruitX && headY == fruitY)
        {
            score += 10;
            nTail++;

            fruitX = rand.Next(1, width - 1);
            fruitY = rand.Next(1, height - 1);
        }

        if (((dir == "LEFT" && pre_dir != "UP") && (dir == "LEFT" && pre_dir != "DOWN")) ||
          ((dir == "RIGHT" && pre_dir != "UP") && (dir == "RIGHT" && pre_dir != "DOWN")))
        {
            horizontal = true;
        }

        else
        {
            horizontal = false;
        }

        if (((dir == "UP" && pre_dir != "LEFT") && (dir == "UP" && pre_dir != "RIGHT")) ||
          ((dir == "DOWN" && pre_dir != "LEFT") && (dir == "RIGHT" && pre_dir != "RIGHT")))
        {
            vertical = true;
        }
        else
        {
            vertical = false;
        }


        for (int i = 1; i < nTail; i++)
        {
            if (TailX[i] == headX && TailY[i] == headY)
            {
                if (horizontal || vertical)
                {
                    gameOver = false;
                }

                else
                {
                    gameOver = true;
                }
            }

            if (TailX[i] == fruitX && TailY[i] == fruitY)
            {
                fruitX = rand.Next(1, width - 1);
                fruitY = rand.Next(1, height - 1);
            }
        }
    }


    void Render()

    {
        Console.SetCursorPosition(0, 0);

        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                if (i == 0 || i == height - 1)
                {
                    Console.Write("#");
                }

                else if (j == 0 || j == width - 1)
                {
                    Console.Write("#");
                }
                else if (j == fruitX && i == fruitY)
                {
                    Console.Write("F");
                }

                else if (j == headX && i == headY)
                {
                    Console.Write("0");
                }

                else
                {
                    isprinted = false;

                    for (int k = 0; k < nTail; k++)
                    {
                        if (TailX[k] == j && TailY[k] == i)
                        {
                            Console.Write("o");
                            isprinted = true;
                        }
                    }

                    if (!isprinted)
                        Console.Write(" ");
                }
            }
            Console.WriteLine();
        }

        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(" YOUR SCORE : " + score);
    }


    void Lose()
    {
        Console.CursorTop = height + 3;
        Console.CursorLeft = width / 2 - 4;
        Console.WriteLine("YOU DIED");
        Console.WriteLine(" R to reset");
        Console.Write(" Esc to quit");

        while (true)
        {
            keypress = Console.ReadKey(true);

            if (keypress.Key == ConsoleKey.Escape)
            {
                Environment.Exit(0);
            }

            if (keypress.Key == ConsoleKey.R)
            {
                reset = true;
                break;
            }
        }
    }

    void Update()
    {
        while (!gameOver)
        {
            CheckInput();
            Logic();
            Render();
            if (reset)
                break;
            Thread.Sleep(30);
        }
        if (gameOver)
            Lose();
    }


    static void Main(string[] args)
    {
        Snake snake = new Snake();
        snake.ShowBanner();
        while (true)
        {
            snake.Setup();
            snake.Update();

            Console.Clear();
        }
    }
}

}

Vuqar Rahimli
  • 11
  • 1
  • 4
  • Does headX and headY move when it still is in for loop? if not then TailX[i] and TailY[i] are always have to be equal to [0] right? if so how does parts follow head? HELPP – Vuqar Rahimli Nov 26 '20 at 12:06
  • The `Logic` function is quite logical :) - first it temporarily stores the position of the head of the snake in `preX/preY`. It then sets the first element in the `TailX/Y` array to the position of the head (which will have been updated to a new position from the previous logic loop based on what direction you pressed). It then loops over each element and sets the new position to be the position of the element before it. That way, the head is in the right place, and the first segment of the tail is just where the head was last loop, and each segment is where the preceding segment was last loop – Charleh Nov 26 '20 at 12:17
  • Think of it like "after we got our new head position, for each segment after that, set it's position to where the segment after it was last frame" - each segment updates into the X/Y position of the segment after it. The reason for the temp variable is to track the previous segments position so it can be swapped - you need a temp variable when you swap elements in an array. e.g. https://stackoverflow.com/questions/872310/javascript-swap-array-elements (look at the first code block in the accepted answer) – Charleh Nov 26 '20 at 12:22
  • where I am having trouble is, when we get our first body part which is TailX/Y[1], we store its value to temp which is 0 right? then we pass preX/Y to TailX/Y which is TailX/Y[0] values, then again we pass temp to pre which is 0 again. So in every loop pre becomes 0 again, doesnt it? I think so anyway. I know Im making a mistake here but dont know where. – Vuqar Rahimli Nov 26 '20 at 12:35
  • I mean initially all we have for TailX/Y[1] is 0 before we assign preX/Y to it in the for loop. So we give its value which is 0 to temp, then we pass preX and Y values to Tail, but our temp is still 0, then we pass temps value which is 0 to pre. where am I making the mistake here – Vuqar Rahimli Nov 26 '20 at 12:42
  • Yes you are correct - `TailX[i]` (where `i >= 1`) will be zero the first time round the loop - but why does that matter? The head position gets stored in element `[0]` and the loop only looks into `TailX/TailY` to the length of `nTail` (which is the length of the snake). So we don't do any updates on the first frame, but later on, `nTail` increases by 1 (when you get food) and the loop remembers the head position from the last frame and copies that into element 1 becoming the tail position, the head is now in the new position in element 0. – Charleh Nov 26 '20 at 12:55
  • Remember `nTail` starts at zero. `nTail` is the tail length, not the snake length. So if you look at the loop condition `for (int i = 1; i < nTail; i++)` - this never gets executed on the first frame. – Charleh Nov 26 '20 at 12:58
  • So the loop only works after we have eaten at least 2 foods, right? after we ate first food, since we havent entered the loop, how does that gets in the right place? – Vuqar Rahimli Nov 26 '20 at 13:02
  • I mean how does it know to be on TailX/Y[1] since we dont have any values for it? – Vuqar Rahimli Nov 26 '20 at 13:06
  • Because `TailX` is the positions of the tail, not the head. The `Logic` loop always sets the position of the first element in the tail to the position of the head on the last frame. The renderer always renders a `0` at the position of `headX/headY` and loops from element `[0]` in the `TailX/Y` array to the length of `nTail` and renders a `o` for each tail element. It's actually a strange approach but I suppose it works fine. I probably wouldn't have written it quite like this - but there are many ways to achieve the same result. – Charleh Nov 26 '20 at 13:07
  • frame means every time logic runs right? – Vuqar Rahimli Nov 26 '20 at 13:10
  • Yes, frame is every time the `Logic` function runs. I think you are getting confused because `Logic` *always* sets a position for `TailX/Y[0]` but the `Render` function doesn't use it until `nTail` is 1 (because `TailX[0]` is the first **tail** position, not the head of the snake) – Charleh Nov 26 '20 at 13:12
  • so every time we pass headX/Y values to TailX/Y[0] our head is actually one frame ahead from the value we pass? – Vuqar Rahimli Nov 26 '20 at 13:13
  • Yes (because the update for position of headX/Y happens *after* the tail is updated). – Charleh Nov 26 '20 at 13:15
  • Also, it's pretty buggy - you can eat your own tail and not die (most of the time), and some of the walls disappear if you go backwards on yourself (which isn't allowed in snake)! :) – Charleh Nov 26 '20 at 13:16
  • yeah I have been encountering those too, but I just wanna grasp how the algorithm works here – Vuqar Rahimli Nov 26 '20 at 13:19
  • can you explain together with values of pre, temp, head, and Tail[i], when i is 0,1 and 2. I think that will clear my head. Like what is those values when i is 0,1,2? you can make up head values of course, just I wanna know what happens with pre temp and tail[i] during that – Vuqar Rahimli Nov 26 '20 at 13:23
  • I'll write up an answer later - just busy at the mo :) – Charleh Nov 26 '20 at 14:03
  • Okay, thanks a lot! – Vuqar Rahimli Nov 26 '20 at 14:28

1 Answers1

0

Ok so let's run through the Logic function with different values of i.

i represents the number of loops to do in the Logic function up to the length of the snake which is given by nTail, however, it starts from 1, not 0 so it skips the first tail segment which is always set to where the head was last frame.

When the snake is just a head nTail = 0

Before you eat food, nTail is 0 and therefore the only part of the logic that runs is:

// Set the position of the first segment of the tail to the position of where the head was this frame
TailX[0] = headX;
TailY[0] = headY;

and then a bit later in the function

// Set a new position for the head of the snake based on direction
switch (dir)
{
    case "RIGHT":
        headX++;
    break;

    // etc...

In this example, assuming the snake is moving right (let's ignore Y and just look at X).

headX = 5;
TailX[0] = 4;

Let's now assume you ate some food so nTail = 1

When the snake has 1 segment of tail nTail = 1

The same thing happens as in the first state - the loop is skipped.

Since the function is already setting the position of the tail to be the position of the head, and then updating the head, we still have the same situation, except we are just moved along another tile (update TailX[0] to position of headX, then update headX based on the direction we are travelling).

headX = 6;
TailX[0] = 5;

What happens now when you get more food on the next turn (apart from being lucky)?

When the snake has 2 segments of tail nTail = 2

Now the Logic loop runs the tail update loop because nTail is now large enough for the loop condition to trigger.

Now remember that we already updated TailX[0] to be the same as headX - so we know that the first segment of the tail is already in the place that the head was last frame.

Now we loop through all of the other tail segments and set their position to the place where the previous tail segment was.

This is why preX is important. At the start of the function we capture the position of the first tail segment preX = TailX[0].

Then when we get to the loop. We only loop once because nTail = 1, so (I've excluded the Y as we only need X to demonstrate)

for (int i = 1; i < nTail; i++)
{
    // Capture the X position of the current segment of tail as we will need it to update the next segment of tail 
    tempX = TailX[i];
    // Set the X position of the tail to the previous tail segment (remember, we captured this at the start of the logic function and it was essentially the X position of the head)
    TailX[i] = preX;
    // Capture the old position of the tail segment pre-update so we can assign it to the next tail segment
    preX = tempX;
}

// Later on we update the head to its new position
 

So this results in:

headX = 7;
TailX[0] = 6;
TailX[1] = 5;

So you have to think about the algorithm backwards as that's how it works:

  • Update the first segment of tail to where the head is
  • Update any other segments of tail to where the previous segment was
  • Update the head to move to a new position

Important to also note that the render function always renders the head using headX/Y and only renders the tail if nTail is greater than 0.

This explains why, even though TailX[0] is set every frame regardless of the snake length, you don't see the tail until you eat food.

Addendum:

Couple of bugs that exist:

  • You can go backwards and run into your tail - this sometimes kills you, but most of the time doesn't. If your head is on a part of your tail, some of the arena walls don't render. I've not looked at why but I noticed this in gameplay.

  • The play area is 20 * 60, if we subtract 1 from each dimension (top/bottom/left/right) for the walls, we end up with a playable area of 18 * 58 which means the max length of the snake can technically be 1,044. The arrays aren't big enough and getting to a length of 101 will crash the game (index out of range) - I spotted this in the code and recreated the scenario in VS:

oops, bugs!

Charleh
  • 13,749
  • 3
  • 37
  • 57