5
\$\begingroup\$

So about a week ago I decided to write a simple console version of 2048 game. Well, as you'll see, it came out not that simple... And took a lot more time and practice than expected (should've done it with 2d array...). But anyway here's a code, and I want to know how can it be optimized and improved? I want to know my mistakes and how it can be done easier. If you have some general tips on coding style, readability or anything else I'd appreciate your help.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace Game
{
    class Program
    {
        static void Main()
        {
            Console.CursorVisible = false;

            do
            {
                Game game = new Game(4, 8, 2);

                string key;
                do
                {
                    Console.WriteLine("1. New game");
                    Console.WriteLine("2. Quit");
                    key = Console.ReadLine();
                    Console.Clear();
                } while (key != "1" && key != "2");

                switch (key)
                {
                    case "1":
                        game.Run();
                        break;
                    case "2":
                        Environment.Exit(0);
                        break;
                }
                Console.Clear();
            } while (true);
        }
    }

    public enum Direction
    {
        Left,
        Right,
        Up,
        Down
    }

    class Cell : IEquatable<Cell>, ICloneable
    {
        public int Value { get; set; }
        public int X { get; set; }
        public int Y { get; set; }

        public Cell(int value, int x, int y)
        {
            Value = value;
            X = x;
            Y = y;
        }

        // Double the value
        public void Double() => Value *= 2;

        // Display value of cell at certain coordinates
        public void Display()
        {
            Console.SetCursorPosition((Y - 1) * 5 + 1, (X - 1) * 2 + 1);
            ColorChanger.ColorCell(Value.ToString());
        }

        // Erase value of cell at certain coordinates
        public void Erase()
        {
            Console.SetCursorPosition((Y - 1) * 5 + 1, (X - 1) * 2 + 1);
            ColorChanger.ColorCell("    ");
        }

        #region overriding section

        public static bool operator ==(Cell cell1, Cell cell2)
        {
            return cell1.Value == cell2.Value && cell1.X == cell2.X && cell1.Y == cell2.Y;
        }

        public static bool operator !=(Cell cell1, Cell cell2)
        {
            return !(cell1.Value == cell2.Value && cell1.X == cell2.X && cell1.Y == cell2.Y);
        }

        public bool Equals(Cell cell)
        {
            if (cell is null)
                return false;

            return Value == cell.Value && X == cell.X && Y == cell.Y;
        }

        public override bool Equals(object obj)
        {
            Cell cell = obj as Cell;
            if (obj == null)
                return false;
            return Value == cell.Value && X == cell.X && Y == cell.Y;
        }

        public override int GetHashCode()
        {
            return Value.GetHashCode() ^ X.GetHashCode() ^ Y.GetHashCode();
        }

        public object Clone() => MemberwiseClone();

        #endregion
    }

    class Game
    {
        // Length & Width of game field (doesn't have to be equal)
        int length;
        int width;

        int Length
        {
            get => length;
            set
            {
                if (value >= 2)
                    length = value;
                else
                    length = 2;
            }
        }
        int Width
        {
            get => width;
            set
            {
                if (value >= 2)
                    width = value;
                else
                    width = 2;
            }
        }

        // Stats of current game
        int Score { get; set; }
        static int Highscore { get; set; }
        int Moves { get; set; }

        // List of current cells & cells on previous move
        List<Cell> cells;
        List<Cell> prevCells;

        Random rand = new Random();

        // Initializing the game with field parameters and cell amount at the beginning
        public Game(int length, int width, int initialCellAmount)
        {
            cells = new List<Cell>();
            prevCells = new List<Cell>();
            Length = length;
            Width = width;
            Score = 0;
            Moves = 0;

            for (int i = 0; i < initialCellAmount; i++)
                AddCell();
        }

        // Launching the game
        public void Run()
        {
            DisplayField();
            DisplayCells();
            DisplayStats();

            do
            {
                if (Console.KeyAvailable)
                {
                    HandleKey();
                    if (IsMoved())
                        AddCell();
                    DisplayCells();
                    DisplayStats();

                    bool isOver = IsOver();
                    bool isWon = IsWon();

                    if (isOver || isWon)
                    {
                        Console.SetCursorPosition(0, 11);
                        Console.WriteLine(isOver ? "You lost!" : "You won!");
                        Thread.Sleep(2000);
                        break;
                    }
                }
            } while (true);
        }

        // Checking for any cell on field that moved
        bool IsMoved()
        {
            foreach (Cell cell in prevCells)
            {
                if (cells.Contains(cell) == false)
                {
                    Moves++;
                    return true;
                }
            }
            return false;
        }

        // Adding new cell on the field (90% - '2', 10% - '4')
        void AddCell()
        {
            if (IsFieldFull())
                return;
            Cell cell;
            do
            {
                cell = new Cell(rand.NextDouble() < 0.9 ? 2 : 4, rand.Next(1, Length), rand.Next(1, Width));
            } while (cells.Any(c => c.X == cell.X && c.Y == cell.Y));
            cells.Add(cell);
        }

        // Drawing the field
        void DisplayField()
        {
            for (int i = 0; i < Length + 1; i++)
            {
                for (int j = 0; j < Width; j++)
                    Console.Write(" ----");

                Console.WriteLine();

                if (i == Length)
                    break;

                for (int j = 0; j < Width + 1; j++)
                    Console.Write("|    ");

                Console.WriteLine();
            }
        }

        // Displaying existing cells
        void DisplayCells()
        {
            foreach (Cell cell in cells)
                cell.Display();
        }

        // Displaying score, highscore and moves
        void DisplayStats()
        {
            Console.SetCursorPosition(0, 9);
            Console.WriteLine($"Score: {Score}   Highscore: {Highscore}\n" +
                $"Moves: {Moves}");
        }

        // Cells movement algorithm
        void MoveCells(Direction dir)
        {
            // Erasing existing cells on field
            foreach (Cell cell in cells)
                cell.Erase();

            // Choosing type of order based on chosen Direction
            switch (dir)
            {
                case Direction.Left:
                    cells = cells.OrderBy(c => c.X).ThenBy(c => c.Y).ToList();
                    break;
                case Direction.Right:
                    cells = cells.OrderBy(c => c.X).ThenByDescending(c => c.Y).ToList();
                    break;
                case Direction.Up:
                    cells = cells.OrderBy(c => c.Y).ThenBy(c => c.X).ToList();
                    break;
                case Direction.Down:
                    cells = cells.OrderBy(c => c.Y).ThenByDescending(c => c.X).ToList();
                    break;
                default:
                    return;
            }

            // Get X or Y coordinate of certain cell
            int getCoord(Cell cell, bool isOpposite)
            {
                return isOpposite ^ (dir == Direction.Left || dir == Direction.Right) ? cell.Y : cell.X;
            }

            // Set X or Y coordinate of certain cell
            void setCoord(ref Cell cell, int value)
            {
                if (dir == Direction.Left || dir == Direction.Right)
                    cell.Y = value;
                else
                    cell.X = value;
            }

            // Assigning first value for every enumerated column or row
            int border = 0;
            switch (dir)
            {
                case Direction.Left:
                    border = 1;
                    break;
                case Direction.Right:
                    border = Width;
                    break;
                case Direction.Up:
                    border = 1;
                    break;
                case Direction.Down:
                    border = Length;
                    break;
            }

            // Assigning the number, that will be added to certain coordinate
            int n = dir == Direction.Left || dir == Direction.Up ? 1 : -1;

            for (int i = -1; i + 1 < cells.Count(); i++)
            {
                Cell getCurr() => i >= 0 ? cells[i] : null;
                Cell getNext() => cells[i + 1];

                if (i == -1 || getCoord(getCurr(), true) != getCoord(getNext(), true))
                {
                    Cell temp = getNext();
                    setCoord(ref temp, border);
                    continue;
                }

                if (getCurr().Value == getNext().Value)
                {
                    getCurr().Double();
                    CalculateScore(getCurr().Value);
                    cells.Remove(getNext());
                    i--;
                }
                else
                {
                    Cell temp = getNext();
                    setCoord(ref temp, getCoord(getCurr(), false) + n);
                }
            }
        }

        // Setting new score and highscore
        void CalculateScore(int value)
        {
            Score += value;
            if (Score > Highscore)
                Highscore = Score;
        }

        // Choosing movement direction of corresponding key 
        void HandleKey()
        {
            prevCells = cells.Select(c => (Cell)c.Clone()).ToList();
            ConsoleKeyInfo cki = Console.ReadKey(true);
            switch (cki.Key)
            {
                case ConsoleKey.LeftArrow:
                    MoveCells(Direction.Left);
                    break;
                case ConsoleKey.RightArrow:
                    MoveCells(Direction.Right);
                    break;
                case ConsoleKey.UpArrow:
                    MoveCells(Direction.Up);
                    break;
                case ConsoleKey.DownArrow:
                    MoveCells(Direction.Down);
                    break;
            }
        }

        // Checking for field overflow
        bool IsFieldFull()
        {
            return cells.Count() == Length * Width;
        }

        // Checking if there is no cell that can be moved
        bool IsOver()
        {
            if (!IsFieldFull())
                return false;

            cells = cells.OrderBy(c => c.X).ThenBy(c => c.Y).ToList();

            int[,] values = new int[Length, Width];

            foreach (Cell cell in cells)
            {
                values[cell.X - 1, cell.Y - 1] = cell.Value;
            }

            for (int i = 0; i < Length; i++)
            {
                for (int j = 0; j < Width; j++)
                {
                    int center = values[i, j];
                    int right = j + 1 < Width ? values[i, j + 1] : 0;
                    int bottom = i + 1 < Length ? values[i + 1, j] : 0;

                    if (center == right || center == bottom)
                        return false;
                }
            }
            return true;
        }

        // Searching for cell with 2048
        bool IsWon()
        {
            return cells.Any(c => c.Value == 2048);
        }
    }

    // Class for changing color for cells
    static class ColorChanger
    {
        // Get color for corresponding value of cell
        static ConsoleColor GetCellColor(string value)
        {
            switch (value)
            {
                case "2":
                    return ConsoleColor.Blue;
                case "4":
                    return ConsoleColor.Magenta;
                case "8":
                    return ConsoleColor.Cyan;
                case "16":
                    return ConsoleColor.Green;
                case "32":
                    return ConsoleColor.Yellow;
                case "64":
                    return ConsoleColor.DarkBlue;
                case "128":
                    return ConsoleColor.DarkMagenta;
                case "256":
                    return ConsoleColor.DarkCyan;
                case "512":
                    return ConsoleColor.DarkGreen;
                case "1024":
                    return ConsoleColor.DarkYellow;
                default:
                    return ConsoleColor.Red;
            }
        }

        // Color certain cell with optional background color
        public static void ColorCell(string value, ConsoleColor bgColor = ConsoleColor.Black)
        {
            ConsoleColor defaultFg = Console.ForegroundColor;
            ConsoleColor defaultBg = Console.BackgroundColor;

            Console.ForegroundColor = GetCellColor(value);
            Console.BackgroundColor = bgColor;

            Console.WriteLine(value);

            Console.ForegroundColor = defaultFg;
            Console.BackgroundColor = defaultBg;
        }
    }
}
\$\endgroup\$
4
  • 2
    \$\begingroup\$ Bravo, well done! The code is very readable and well structured. \$\endgroup\$ Commented Feb 24, 2018 at 15:26
  • \$\begingroup\$ @OlivierJacot-Descombes, thanks, but aside of that I still think there is a room for improvement :) \$\endgroup\$
    – Glitch
    Commented Feb 24, 2018 at 15:33
  • \$\begingroup\$ The code is very readable and well structured. you think? I'm not so sure about it... \$\endgroup\$
    – t3chb0t
    Commented Feb 25, 2018 at 15:30
  • \$\begingroup\$ @t3chb0t then provide some clarification, what you think is wrong and needs to be improved? \$\endgroup\$
    – Glitch
    Commented Feb 25, 2018 at 15:42

1 Answer 1

4
\$\begingroup\$

Bravo, well done! The code is very readable and well structured.

If you intend to make several console games, you will notice that some things remain the same. To allow reusability I suggest abstracting certain aspects through interfaces. It also reduces dependencies between components, enhances testability and allows you to easily replace a component by another one.

public interface IMainUserInterface
{
    void StartGame(IGameLoop gameLoop, IGame game);
}

public interface IGameLoop
{
    void Run(IGame game);
}

public interface IGame
{
    void DisplayField();
    void DisplayCells();
    void DisplayStats();
    void MakeMove(ConsoleKeyInfo cki);
    bool IsOver();
    bool IsWon();
}

The IMainUserInterface implementation:

public class MainUserInterface : IMainUserInterface
{
    public void StartGame(IGameLoop gameLoop, IGame game)
    {
        Console.CursorVisible = false;
        do
        {
            string key;
            do
            {
                Console.WriteLine("1. New game");
                Console.WriteLine("2. Quit");
                key = Console.ReadLine();
                Console.Clear();
            } while (key != "1" && key != "2");
            switch (key)
            {
                case "1":
                    gameLoop.Run(game);
                    break;
                case "2":
                    Environment.Exit(0);
                    break;
            }
            Console.Clear();
        } while (true);
    }
}

The IGameLoop implementation:

public class GameLoop : IGameLoop
{
    void Run(IGame game)
    {
        game.DisplayField();
        game.DisplayCells();
        game.DisplayStats();

        do
        {
            if (Console.KeyAvailable)
            {
                game.MakeMove(Console.ReadKey(true));
                game.DisplayCells();
                game.DisplayStats();

                bool isOver = game.IsOver();
                bool isWon = game.IsWon();

                if (isOver || isWon)
                {
                    Console.SetCursorPosition(0, 11);
                    Console.WriteLine(isOver ? "You lost!" : "You won!");
                    Thread.Sleep(2000);
                    break;
                }
            }
        } while (true);
    }
}

I won't show the game implementation here. The Main method becomes

class Program
{
    static void Main()
    {
        IMainUserInterface userInterface = new MainUserInterface();
        IGameLoop gameLoop = new GameLoop();
        IGame game = new Game2048();

        userInterface.StartGame(gameLoop, game);
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Thank you, I will definitely include interfaces in my next projects :) \$\endgroup\$
    – Glitch
    Commented Feb 24, 2018 at 18:05

Not the answer you're looking for? Browse other questions tagged or ask your own question.