C# Змейка в консоли

Я создал Snake в консоли. Я пытался сделать свой код как можно лучше. (Код на гитхабе: https://github.com/bartex-bartex/SnakeCSharp)

Вопрос

Достаточно ли хорош мой стиль кодирования, чтобы я мог перейти к созданию другого проекта, не повторяя при этом некоторых ошибок, которые я сделал здесь?

Программа.cs
using SnakeGame;

Game game = new Game(40, 30);
game.Start();
Game.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SnakeGame
{
    public class Game
    {
        public Board Board { get; set; }
        public Snake Snake { get; set; }
        public Apple Apple { get; set; }
        public Score Score { get; set; }

        private const int scoreTextHeight = 1;
        private const int FPS = 16; 


        public Game(int width, int height)
        {
            Board = new Board(width, height);
            Snake = new Snake(width, height);
            Apple = new Apple(width, height, Snake.Position);
            Score = new Score();

            Console.CursorVisible = false;
            Console.SetWindowSize(width, height + scoreTextHeight);
        }

        public void Start()
        {
            Point previousSnakeTail = new Point(Snake.GetSnakeTail().x, Snake.GetSnakeTail().y);
            while (true)
            {
                Board.Draw(Snake.Position, previousSnakeTail, Apple.Position);
                previousSnakeTail = Snake.GetSnakeTail();

                // Snake Movement
                Direction direction = KeyboardManager.GetDirection();
                bool isBodyCollision = Snake.Move(direction, Board.Width, Board.Height);
                if (isBodyCollision) break;
                

                // Handle apple eat
                if (Apple.CheckIfEaten(Snake.GetSnakeHead()))
                {
                    Snake.IncreaseLength();
                    Score.IncreasePoints();

                    Apple = new Apple(Board.Width, Board.Height, Snake.Position);
                }

                Score.PrintScore(Board.Height);
                Thread.Sleep(1000 / FPS);
            }

            Messages.GameOverMessage(Board.Width, Board.Height, Score.GetPoints());
            Console.ReadLine();
        }
    }
}
Змея.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Text;
using System.Threading.Tasks;

namespace SnakeGame
{
    public struct Point
    {
        public int x;
        public int y;

        public Point(int x, int y)
        {
            this.x = x;
            this.y = y;
        }

        public static bool operator== (Point p1, Point p2)
        {
            if(p1.x == p2.x && p1.y == p2.y)
            {
                return true;
            }
            return false;
        }

        public static bool operator !=(Point p1, Point p2)
        {
            if (p1.x != p2.x || p1.y != p2.y)
            {
                return true;
            }
            return false;
        }
    }

    public enum Direction { previousDirection, up, right, down, left }


    public class Snake
    {
        public List<Point> Position { get; private set; }
        private Direction currentDirection;

        public Snake(int mapWidth, int mapHeight)
        {
            Position = new List<Point>();

            //Spawn snake head in the middle
            Position.Add(new Point(mapWidth / 2, mapHeight / 2));

            //Initiate movement
            currentDirection = Direction.up;
        }

        public bool Move(Direction newDirection, int mapWidth, int mapHeight)
        {
            currentDirection = UpdateCurrentDirection(newDirection);

            switch (currentDirection)
            {
                case Direction.up:
                    UpdateSnakePositionList(Direction.up, mapWidth, mapHeight, 
                        new Point(Position[0].x, mapHeight - 2), new Point(Position[0].x, Position[0].y - 1));
                    break;
                case Direction.right:
                    UpdateSnakePositionList(Direction.right, mapWidth, mapHeight,
                        new Point(1, Position[0].y), new Point(Position[0].x + 1, Position[0].y));
                    break;
                case Direction.down:
                    UpdateSnakePositionList(Direction.down, mapWidth, mapHeight,
                        new Point(Position[0].x, 1), new Point(Position[0].x, Position[0].y + 1));
                    break;
                case Direction.left:
                    UpdateSnakePositionList(Direction.left, mapWidth, mapHeight,
                        new Point(mapWidth - 2, Position[0].y), new Point(Position[0].x - 1, Position[0].y));
                    break;
                default:
                    break;
            }
            return CheckIfCollisionWithTail();
        }

        private Direction UpdateCurrentDirection(Direction newDirection)
        {
            if (newDirection != Direction.previousDirection)
            {
                if (Position.Count == 1)
                { 
                    return newDirection;
                }
                if (CheckIfOppositeDirection(newDirection) == true)
                {
                    return currentDirection;
                }
                else
                {
                    return newDirection;
                }
            }
            else
            {
                return currentDirection;
            }
        }

        private bool CheckIfOppositeDirection(Direction newDirection)
        {
            if (currentDirection == Direction.left && newDirection == Direction.right ||
                currentDirection == Direction.right && newDirection == Direction.left ||
                currentDirection == Direction.up && newDirection == Direction.down ||
                currentDirection == Direction.down && newDirection == Direction.up)
            {
                return true;
            }
            return false;
        }

        private bool CheckIfCollisionWithBoundary(Direction direction, int mapWidth, int mapHeight)
        {
            switch (direction)
            {
                case Direction.up:
                    if (Position[0].y - 1 == 0)
                    {
                        return true;
                    }
                    return false;
                case Direction.right:
                    if (Position[0].x + 1 == mapWidth - 1)
                    {
                        return true;
                    }
                    return false;
                case Direction.down:
                    if (Position[0].y + 1 == mapHeight - 1)
                    {
                        return true;
                    }
                    return false;
                case Direction.left:
                    if (Position[0].x - 1 == 0)
                    {
                        return true;
                    }
                    return false;
                default:
                    return false;
            }
        }

        private void UpdateSnakePositionList(Direction direction, int mapWidth, int mapHeight, Point newHeadPointIfBorderHit, Point newHeadPointIfNormalMove)
        {
            if (CheckIfCollisionWithBoundary(direction, mapWidth, mapHeight) == true)
            {
                Position.Insert(0, newHeadPointIfBorderHit);
            }
            else
            {
                Position.Insert(0, newHeadPointIfNormalMove);
            }
            Position.RemoveAt(Position.Count - 1);
        }

        private bool CheckIfCollisionWithTail()
        {
            if (Position.Skip(1).Contains(Position[0]))
            {
                return true;
            }
            return false;
        }
        
        public void IncreaseLength()
        {
            Position.Add(Position[Position.Count - 1]);
        }
        public Point GetSnakeHead()
        {
            return Position[0];
        }
        public Point GetSnakeTail()
        {
            return Position[^1];
        }
    }
}
Board.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SnakeGame
{

    public class Board
    {
        public readonly int Width;
        public readonly int Height;

        public Board(int width, int height)
        {
            Width = width;
            Height = height;
        }

        public void Draw(List<Point> snakePosition, Point previousSnakeTail, Point applePosition)
        {
            DrawBoard();

            DrawSnake(snakePosition, previousSnakeTail);

            DrawApple(applePosition);
        }

        private static void DrawApple(Point applePosition)
        {
            Console.SetCursorPosition(applePosition.x, applePosition.y);
            Console.BackgroundColor = ConsoleColor.Red;
            Console.Write("@");
            Console.BackgroundColor = ConsoleColor.Black;
        }

        private static void DrawSnake(List<Point> snakePosition, Point previousSnakeTail)
        {
            for (int i = 0; i < snakePosition.Count; i++)
            {
                Console.SetCursorPosition(snakePosition[i].x, snakePosition[i].y);
                if (i == 0)
                {
                    Console.ForegroundColor = ConsoleColor.Blue;
                    Console.Write("O");
                    Console.ForegroundColor = ConsoleColor.White;
                }
                else
                {
                    Console.Write("o");
                }
            }
            // Instead of redrawing whole Board just clear previous Snake tail
            Console.SetCursorPosition(previousSnakeTail.x, previousSnakeTail.y);
            Console.Write(" ");
        }

        private void DrawBoard()
        {
            Console.SetCursorPosition(0, 0);
            for (int i = 0; i < Width; i++)
            {
                Console.Write("#");
            }

            Console.WriteLine();
            for (int i = 0; i < Height - 2; i++)
            {
                Console.Write("#");
                Console.SetCursorPosition(Width - 1, i + 1);
                Console.WriteLine("#");
            }

            for (int i = 0; i < Width; i++)
            {
                Console.Write("#");
            }
        }
    }
}
Apple.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SnakeGame
{
    public class Apple
    {
        public Point Position { get; private set; }
        public bool IsEaten { get; private set; }

        public Apple(int mapWidth, int mapHeight, List<Point> snakePosition)
        {
            IsEaten = false;

            SpawnApple(mapWidth, mapHeight, snakePosition);
        }

        private void SpawnApple(int mapWidth, int mapHeight, List<Point> snakePosition)
        {
            Random rand = new Random();
            Point applePosition;

            do
            {
                applePosition = new Point(rand.Next(1, mapWidth - 2), rand.Next(1, mapHeight - 2));
            } while (snakePosition.Contains(applePosition) != false);

            Position = applePosition;
        }

        public bool CheckIfEaten(Point SnakeHeadPosition)
        {
            if(SnakeHeadPosition == Position)
            {
                IsEaten = true;
                return true;
            }
            return false;
        }
    }
}
KeyboardManager.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SnakeGame
{
    public static class KeyboardManager
    {
        public static Direction GetDirection()
        {
            if (Console.KeyAvailable)
            {
                ConsoleKeyInfo pressedKey = Console.ReadKey(true);

                ClearBuffer();

                switch (pressedKey.Key)
                {
                    case ConsoleKey.D:
                    case ConsoleKey.RightArrow:
                        return Direction.right;
                    case ConsoleKey.S:
                    case ConsoleKey.DownArrow:
                        return Direction.down;
                    case ConsoleKey.A:
                    case ConsoleKey.LeftArrow:
                        return Direction.left;
                    case ConsoleKey.W:
                    case ConsoleKey.UpArrow:
                        return Direction.up;
                    default:
                        return Direction.previousDirection;
                }
            }
            return Direction.previousDirection;
        }

        private static void ClearBuffer()
        {
            while (Console.KeyAvailable)
            {
                Console.ReadKey(true);
            }
        }
    }
}
Score.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SnakeGame
{
    public class Score
    {
        private int points = 0;
        public void PrintScore(int mapHeight)
        {
            Console.SetCursorPosition(0, mapHeight);
            Console.Write($"Your score: {points}");
        }

        public void IncreasePoints()
        {
            points++;
        }

        public int GetPoints()
        {
            return points;
        }
    }
}
Сообщения.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SnakeGame
{
    public static class Messages
    {
        public static void GameOverMessage(int mapWidth, int mapHeight, int score)
        {
            Console.SetCursorPosition(mapWidth / 4, mapHeight / 2);
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("GAME OVER");
            Console.SetCursorPosition(mapWidth / 4, mapHeight / 2 + 1);
            Console.WriteLine($"Your score was: {score}");
            Console.ForegroundColor = ConsoleColor.White;
        }
    }
}

1 ответ
1

Программа.cs

Отличная работа! Крайне редко можно увидеть хороший объектно-ориентированный подход с соответствующим уровнем абстракции и отделением приложения от точки входа в среду.


Нет public сеттеры

public class Game
{
    public Board Board { get; protected set; }
    public Snake Snake { get; protected set; }
    public Apple Apple { get;  protected set; }
    public Score Score { get;  protected set; }
    

Хороший класс раскрывает функциональность и скрывает состояние. Поскольку код уже написан, они вам не нужны. НИКОГДА публично не раскрывайте внутреннее состояние.


Не это:

       if (Position[0].y - 1 == 0)
                {
                    return true;
                }
                return false;
                

Этот:

    return (Position[0].y - 1 == 0);
    

также методы перегрузки оператора:

       return (p1.x == p2.x && p1.y == p2.y)
       

Легкая техника уменьшения сложности

Немедленно вернитесь на расторжающих условиях. Используйте тернарный оператор. Это снижает оценку технической сложности, потому что в основном это касается общего количества возможных ответвлений.

private Direction UpdateCurrentDirection(Direction newDirection)
{
    if (newDirection == Direction.previousDirection) return currentDirection;
        
    if (Position.Count == 1)  return newDirection;

    return CheckIfOppositeDirection(newDirection)) ? 
              currentDirection : newDirection;
}

Одна линия if без скобок суперразмерная скорость понимания. Секрет в том, чтобы оставить пустое пространство сверху и снизу.

ДО:

private Direction UpdateCurrentDirection(Direction newDirection)
{
    if (newDirection != Direction.previousDirection)
    {
        if (Position.Count == 1)
        { 
            return newDirection;
        }
        if (CheckIfOppositeDirection(newDirection) == true)
        {
            return currentDirection;
        }
        else
        {
            return newDirection;
        }
    }
    else
    {
        return currentDirection;
    }
}

Пока правда верна, эта истинная правда верна

 public void Start()
    {
        // ...
         while (true) {
             // ...
             bool isBodyCollision = Snake.Move(direction, Board.Width, Board.Height);
            if (isBodyCollision) break;
           // ...
        }

Назовите правила игры. Двигаться isBodyCollision объявление вне цикла.

      bool isBodyCollision = false
       while (! isBodyCollision) {
             // ...
            isBodyCollision = Snake.Move(direction, Board.Width, Board.Height);
            if (isBodyCollision) continue;
           // ...
        }
 

Пусть цикл условно управляет выполнением. Это, как правило, помогает предотвратить временную связь по мере изменения кода и увеличения сложности.


Дырявая абстракция

Мне нравится имя переменной «Позиция»

public class Snake {  ...  Position = new List<Point>();  ... }

Играя в игру, мы говорим о позиции Змеи, а не о наборе позиций. Но тогда абстракция несколько пропускает свою реализацию с именем метода:

UpdateSnakePositionList

Измените это на

UpdateSnakePosition
Абстрагирование — это черепахи на всем пути вниз

Каждый уровень структуры кода, метода и класса дает возможность абстрагироваться от реальной терминологии, концепций и игрового процесса Snake Game. Как правило, используйте имена и глаголы, абстрагируясь от реальной реализации кода, а не цепляясь за нее.

Ниже требуется прочитать код цикла и часто читать глубже, чтобы убедиться, что я понимаю, что происходит «на самом деле»:

while (true) {
   // ....
}

Все эти усилия исчезают, когда:

while (! isBodyCollision) {
   // ....
}

Имя класса дает контекст методам

Этот: Draw Нет: DrawBoard

Краткое удовольствие от чтения:

    Board snakeBoard = new Board(...);
    snakeBoard.Draw();
    

переключение регистра и сквозное форматирование

Всегда ставьте пробел между case перерыв/возврат и следующий case. В частности, случаи провала выделяются намного лучше.

       case ConsoleKey.D:
       case ConsoleKey.RightArrow:
             return Direction.right;
        
        case ConsoleKey.S:
        case ConsoleKey.DownArrow:
              return Direction.down;
                
        case ConsoleKey.A:
        

Проверьте «проверить» в именах методов

«Чек» — это такой общий термин, что он ничего не значит. Но я признаю, что иногда не могу с собой поделать. Очень постарайтесь заменить слово «чек» на что-то другое. Легче исправить при возврате логического значения с префиксом «is»

  • CheckIfEatenIsEaten
  • CheckIfOppositeDirectionChangedDirection
  • CheckIfCollisionWithBoundaryIsBoundryCollision

Краткое удовольствие от чтения:

if ( Apple.IsEaten())
if (ChangedDirection())

Получить это данность

Такие методы: GetSnakeHead(). После примерно 7602 методов с префиксом GET вы поймете, что это излишне. SnakeHead() лучше. Сделать это собственностью — Snake.Head еще лучше.

Краткое удовольствие от чтения:

  if (Apple.IsEaten(Snake.Head))
  

О, о. Теперь я вижу проблему. Не шучу — я подумал об этом только после того, как написал это. Код кажется «обратным», и я думаю, что это «инверсия управления».


Инверсия управления (IOC)

 public class Start {
     .....
       if (Apple.CheckIfEaten(Snake.GetSnakeHead())) 
     ....
} // Start

Apple не должна нести ответственность за определение того, съела ли его змея. Это для змеи сделать. Из-за «перевернутой» (назад, наизнанку) ссылки (зависимости) Start класс должен знать Snake внутренние детали, голова против хвоста против других частей. Apple также, для его части.

Другими словами Start должен знать как змея ест, потому что Snake не умеет есть — у него нет пищевого поведения (метода). Если вы когда-нибудь сталкивались с тем, что официант внезапно пихает любую часть вашего тела в тарелку с едой, то вы понимаете, в чем здесь проблема.

Краткое удовольствие от чтения:

    if (Snake.Eats(Apple)) 

Отделите игру от пользовательского интерфейса

Должен быть новый класс, который делает консольные вызовы для взаимодействия с пользовательским интерфейсом. Здесь не будет console код в любом месте в классах Snake Game. Тогда сделайте Game методы/свойства класса для счета, доски и т. д. Класс пользовательского интерфейса взаимодействует с Game объект и никакие другие. Это принцип наименьшего знания.

Предположение: Board.ToString() возвращает доску/сетку в виде строки с/n разделяющие ряды. То же самое для Score.ToString() и любой другой вывод, привязанный к пользовательскому интерфейсу.

Затем Game имеет интерфейс, используемый объектами пользовательского интерфейса.

public class Game {
    public string Score() { return Score.ToString(); }
    public string Board() { return Board.ToString(); } 
    public override string  ToString() {  return Score() + `/n/n` +  Board();   }  
}

Краткое удовольствие от чтения:

 console.WriteLine(mySnakeGame);     // implicit ToString call

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *