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;
 }
 }
}
asked Feb 24, 2018 at 14:55
\$\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\$ 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\$ Commented Feb 25, 2018 at 15:30
  • \$\begingroup\$ @t3chb0t then provide some clarification, what you think is wrong and needs to be improved? \$\endgroup\$ 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);
 }
}
answered Feb 24, 2018 at 16:09
\$\endgroup\$
1
  • \$\begingroup\$ Thank you, I will definitely include interfaces in my next projects :) \$\endgroup\$ Commented Feb 24, 2018 at 18:05

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.