📜 ⬆️ ⬇️

Its a snake, or write the first draft. Part 0

Foreword


Hi Habr! My name is Eugene "Nage", and I started programming about a year ago, during my free time. After reviewing many different tutorials on programming, you ask yourself the question “what to do next?”, Because basically everyone talks about the very basics and as a rule does not go on. But after a long time watching various videos about the same thing, I decided that it was worth moving on and taking up the first project. And so, now we will analyze how you can write the game "Snake" in the console with your initial knowledge.

Chapter 1. So, where do we start?


To begin with, we do not need anything extra, just a notepad (or your favorite editor), and the C # compiler, it is present by default in Windows, it is located in C: \ Windows \ Microsoft .NET \ Framework \ v4.0.30319 \ csc.exe. You can use the latest version of the compiler that comes with visual studio, it is Microsoft Visual Studio \ 2017 \ Community \ MSBuild \ 15.0 \ Bin \ Roslyn \ csc.exe.

Create a file to quickly compile our code, save the file with the extension .bat with the following contents:
')
@echo off :Start set /p name= Enter program name: echo. :\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe "%name%.cs" echo. goto Start 

"@echo off" disables the display of commands in the console. Using the goto command, we get an infinite loop. We set the name variable, and with the / p modifier, the value entered by the user into the console is written to the variable. "Echo." Just leaves an empty line in the console. Next, we call the compiler and pass it the file of our code, which it compiles.

In this way, we can compile only one file, so we will write all the classes in one document (I have not figured out how to compile several files into one .exe through the console, and this is not the topic of our article, can someone tell in the comments).

For those who immediately want to see all the code.

Hidden text
 using System; using System.Threading; using System.Collections.Generic; using System.Linq; namespace SnakeGame { class Game { static readonly int x = 80; static readonly int y = 26; static Walls walls; static Snake snake; static FoodFactory foodFactory; static Timer time; static void Main() { Console.SetWindowSize(x + 1, y + 1); Console.SetBufferSize(x + 1, y + 1); Console.CursorVisible = false; walls = new Walls(x, y, '#'); snake = new Snake(x / 2, y / 2, 3); foodFactory = new FoodFactory(x, y, '@'); foodFactory.CreateFood(); time = new Timer(Loop, null, 0, 200); while (true) { if (Console.KeyAvailable) { ConsoleKeyInfo key = Console.ReadKey(); snake.Rotation(key.Key); } } }// Main() static void Loop(object obj) { if (walls.IsHit(snake.GetHead()) || snake.IsHit(snake.GetHead())) { time.Change(0, Timeout.Infinite); } else if (snake.Eat(foodFactory.food)) { foodFactory.CreateFood(); } else { snake.Move(); } }// Loop() }// class Game struct Point { public int x { get; set; } public int y { get; set; } public char ch { get; set; } public static implicit operator Point((int, int, char) value) => new Point {x = value.Item1, y = value.Item2, ch = value.Item3}; public static bool operator ==(Point a, Point b) => (ax == bx && ay == by) ? true : false; public static bool operator !=(Point a, Point b) => (ax != bx || ay != by) ? true : false; public void Draw() { DrawPoint(ch); } public void Clear() { DrawPoint(' '); } private void DrawPoint(char _ch) { Console.SetCursorPosition(x, y); Console.Write(_ch); } } class Walls { private char ch; private List<Point> wall = new List<Point>(); public Walls(int x, int y, char ch) { this.ch = ch; DrawHorizontal(x, 0); DrawHorizontal(x, y); DrawVertical(0, y); DrawVertical(x, y); } private void DrawHorizontal(int x, int y) { for (int i = 0; i < x; i++) { Point p = (i, y, ch); p.Draw(); wall.Add(p); } } private void DrawVertical(int x, int y) { for (int i = 0; i < y; i++) { Point p = (x, i, ch); p.Draw(); wall.Add(p); } } public bool IsHit(Point p) { foreach (var w in wall) { if (p == w) { return true; } } return false; } }// class Walls enum Direction { LEFT, RIGHT, UP, DOWN } class Snake { private List<Point> snake; private Direction direction; private int step = 1; private Point tail; private Point head; bool rotate = true; public Snake(int x, int y, int length) { direction = Direction.RIGHT; snake = new List<Point>(); for (int i = x - length; i < x; i++) { Point p = (i, y, '*'); snake.Add(p); p.Draw(); } } public Point GetHead() => snake.Last(); public void Move() { head = GetNextPoint(); snake.Add(head); tail = snake.First(); snake.Remove(tail); tail.Clear(); head.Draw(); rotate = true; } public bool Eat(Point p) { head = GetNextPoint(); if (head == p) { snake.Add(head); head.Draw(); return true; } return false; } public Point GetNextPoint () { Point p = GetHead (); switch (direction) { case Direction.LEFT: px -= step; break; case Direction.RIGHT: px += step; break; case Direction.UP: py -= step; break; case Direction.DOWN: py += step; break; } return p; } public void Rotation (ConsoleKey key) { if (rotate) { switch (direction) { case Direction.LEFT: case Direction.RIGHT: if (key == ConsoleKey.DownArrow) direction = Direction.DOWN; else if (key == ConsoleKey.UpArrow) direction = Direction.UP; break; case Direction.UP: case Direction.DOWN: if (key == ConsoleKey.LeftArrow) direction = Direction.LEFT; else if (key == ConsoleKey.RightArrow) direction = Direction.RIGHT; break; } rotate = false; } } public bool IsHit(Point p) { for (int i = snake.Count - 2; i > 0; i--) { if (snake[i] == p) { return true; } } return false; } }//class Snake class FoodFactory { int x; int y; char ch; public Point food { get; private set; } Random random = new Random(); public FoodFactory(int x, int y, char ch) { this.x = x; this.y = y; this.ch = ch; } public void CreateFood() { food = (random.Next(2, x - 2), random.Next(2, y - 2), ch); food.Draw(); } } } 


Chapter 2. First Steps


Let's prepare the field of our game, starting from the entry point to our program. Set the variables X and Y, the size and buffer of the console window, and hide the cursor display.

 using System; using System.Collections.Generic; using System.Linq; class Game{ static readonly int x = 80; static readonly int y = 26; static void Main(){ Console.SetWindowSize(x + 1, y + 1); Console.SetBufferSize(x + 1, y + 1); Console.CursorVisible = false; }// Main() }// class Game 

To display our “graphics” we will create our own data type - point. It will contain the coordinates and the character that will be displayed on the screen. We will also make methods for displaying a point on the screen and its “erasure”.

 struct Point{ public int x { get; set; } public int y { get; set; } public char ch { get; set; } public static implicit operator Point((int, int, char) value) => new Point {x = value.Item1, y = value.Item2, ch = value.Item3}; public void Draw(){ DrawPoint(ch); } public void Clear(){ DrawPoint(' '); } private void DrawPoint(char _ch){ Console.SetCursorPosition(x, y); Console.Write(_ch); } } 

It is interesting!
The => operator is called a lambda operator, it is used as a definition of anonymous lambda expressions, and as a body consisting of a single expression , syntactic sugar, replacing the return operator. The above method for redefining an operator (about its purpose below) can be rewritten as follows:

 public static bool operator == (Point a, Point b) {
     if (ax == bx && ay == by) {
         return true;
     }
     else {
         return false;
     }
 } 


Create a class of walls, the boundaries of the playing field. Let's write 2 methods to create vertical and horizontal lines, and in the constructor we call the drawing of all 4 sides with the specified symbol. A list of all points in the wall will come in handy later.

 class Walls{ private char ch; private List<Point> wall = new List<Point>(); public Walls(int x, int y, char ch){ this.ch = ch; DrawHorizontal(x, 0); DrawHorizontal(x, y); DrawVertical(0, y); DrawVertical(x, y); } private void DrawHorizontal(int x, int y){ for (int i = 0; i < x; i++){ Point p = (i, y, ch); p.Draw(); wall.Add(p); } } private void DrawVertical(int x, int y) { for (int i = 0; i < y; i++) { Point p = (x, i, ch); p.Draw(); wall.Add(p); } } }// class Walls 

It is interesting!

As you might have noticed, the form Point p = (x, y, ch) is used to initialize the Point data type; as with built-in types, this becomes possible when overriding the implicit operator, which describes how variables are set.

Important!

The construction (int, int, char) is called a tuple, and works only with .net 4.7+, so if you have not installed visual studio, then you only have the v4.0.30319 compiler and you need to use standard initialization through the new operator.

Let's go back to the Game class and declare the walls field, and initialize it in the Main method.

 class Game{ static Walls walls; static void Main(){ walls = new Walls(x, y, '#'); ... 

Everything! You can compile the code and see what our field is built, and the easiest part is behind.

Chapter 3. What about breakfast today?


Add the generation of food in our field, for this we create a class FoodFactory, which will be engaged in the creation of food within the borders.

 class FoodFactory { int x; int y; char ch; public Point food { get; private set; } Random random = new Random(); public FoodFactory(int x, int y, char ch) { this.x = x; this.y = y; this.ch = ch; } public void CreateFood() { food = (random.Next(2, x - 2), random.Next(2, y - 2), ch); food.Draw(); } } 

Add factory initialization and create food on the field.
 class Game{ static FoodFactory foodFactory; static void Main(){ foodFactory = new FoodFactory(x, y, '@'); foodFactory.CreateFood(); ... 

Dinner is served!

Chapter 4. The time of the protagonist


Let us proceed to the creation of the snake itself, and for the beginning we will determine the transfer direction of the snake.

 enum Direction{ LEFT, RIGHT, UP, DOWN } 

Now we can create a class of snake, where we describe how it will crawl, turn. We define the list of points of the snake, our enumeration, the step on how much will move during the course, and links to the tail and the head points, and the designer, in which we draw the snake in the given coordinates and given length at the start of the game.

 class Snake{ private List<Point> snake; private Direction direction; private int step = 1; private Point tail; private Point head; bool rotate = true; public Snake(int x, int y, int length){ direction = Direction.RIGHT; snake = new List<Point>(); for (int i = x - length; i < x; i++) { Point p = (i, y, '*'); snake.Add(p); p.Draw(); } } //         . public Point GetHead() => snake.Last(); public void Move(){ head = GetNextPoint(); snake.Add(head); tail = snake.First(); snake.Remove(tail); tail.Clear(); head.Draw(); rotate = true; } public Point GetNextPoint() { Point p = GetHead(); switch (direction) { case Direction.LEFT: px -= step; break; case Direction.RIGHT: px += step; break; case Direction.UP: py -= step; break; case Direction.DOWN: py += step; break; } return p; } public void Rotation(ConsoleKey key) { if (rotate) { switch (direction) { case Direction.LEFT: case Direction.RIGHT: if (key == ConsoleKey.DownArrow) direction = Direction.DOWN; else if (key == ConsoleKey.UpArrow) direction = Direction.UP; break; case Direction.UP: case Direction.DOWN: if (key == ConsoleKey.LeftArrow) direction = Direction.LEFT; else if (key == ConsoleKey.RightArrow) direction = Direction.RIGHT; break; } rotate = false; } } }//class Snake 

In the rotation method, in order to avoid the possibility of turning 180 degrees at once, we simply indicate that in each direction we can only turn in 2 directions. And the problem of turning 180 degrees with two taps - by setting the "switch", we turn off the ability to turn after the first press, and turn it on after the next turn.

It remains to display it on the screen.

 class Game{ static Snake snake; static void Main(){ snake = new Snake(x / 2, y / 2, 3); ... 

Done! now we have everything we need, a field enclosed by walls, randomly appearing food, and a snake. It's time to make it all interact with each other.

Chapter 5. L-Logic


Let's make our snake move, write an infinite loop to read the keys pressed on the keyboard, and pass the key to the snake rotation method

 class Game { static void Main () { while (true) { if (Console.KeyAvailable) { ConsoleKeyInfo key = Console.ReadKey (); snake.Rotation(key.Key); } ... 

for the movement of the snake, we use the class .net which will launch the Loop method at certain intervals.

 using System.Threading; class Game { static Timer time; static void Main () { time = new Timer (Loop, null, 0, 200); ... 

Now, before writing the snake movement method, it is necessary to realize the interaction of the head with the food, walls and tail of the snake. To do this, you need to write a method that allows you to compare two points on the coincidence of coordinates. Let's redefine the operator of equality and non-equality, they must be redefined in a pair.

 struct Point { public static bool operator == (Point a, Point b) => (ax == bx && ay == by) ? true : false; public static bool operator != (Point a, Point b) => (ax != bx || ay != by) ? true : false; ... 

Now you can write a method that will check whether the point of interest coincides with any of the array of walls.
 class Walls { public bool IsHit (Point p) { foreach (var w in wall) { if (p == w) { return true; } } return false; } ... 

And a similar method checking doesn’t match the point with the tail.

 class Snake { public bool IsHit (Point p) { for (int i = snake.Count - 2; i > 0; i--) { if (snake[i] == p) { return true; } } return false; } ... 

And by checking whether our snake ate the food, and immediately make it longer.

 class Snake { public bool Eat (Point p) { head = GetNextPoint (); if (head == p) { snake.Add (head); head.Draw (); return true; } return false; } ... 

Now you can write a method of movement, with all the necessary checks.

 class Snake { static void Loop (object obj) { if (walls.IsHit (snake.GetHead ()) || snake.IsHit (snake.GetHead ())) { time.Change (0, Timeout.Infinite); } else if (snake.Eat (foodFactory.food)) { foodFactory.CreateFood (); } else { snake.Move (); } } ... 

That's all! Our snake in the console is finished and you can play.


Conclusion


We looked at how to implement the first simple game with a little use of OOP, learned how to overload the operators, looked at the tuples and the lambda operator, I hope this was useful!

It was a pilot article, and if you liked it, I will write about the implementation of the snake on Unity.
Good luck to all!

Source: https://habr.com/ru/post/348262/


All Articles