Task

Implement a 2D sliding block puzzle game where blocks with numbers are combined to add their values.



Rules of the game

   The rules are that on each turn the player must choose a direction   (up, down, left or right).
   All tiles move as far as possible in that direction, some move more than others. 
   Two adjacent tiles (in that direction only) with matching numbers combine into one bearing the sum of those numbers. 
   A move is valid when at least one tile can be moved,   if only by combination. 
   A new tile with the value of   2   is spawned at the end of each turn at a randomly chosen empty square   (if there is one). 
   Adding a new tile on a blank space.   Most of the time,   a new   2   is to be added,   and occasionally   (10% of the time),   a   4.
   To win,   the player must create a tile with the number   2048. 
   The player loses if no valid moves are possible.


The name comes from the popular open-source implementation of this game mechanic, 2048.



Requirements

   "Non-greedy" movement.   
  The tiles that were created by combining other tiles should not be combined again during the same turn (move).   
  That is to say,   that moving the tile row of:
               [2][2][2][2] 

 to the right should result in: 
               ......[4][4] 

 and not:
               .........[8] 

   "Move direction priority".   
  If more than one variant of combining is possible,   move direction shall indicate which combination will take effect. 
  For example, moving the tile row of:
               ...[2][2][2] 

 to the right should result in:
               ......[2][4] 

 and not:
               ......[4][2] 



   Check for valid moves.   The player shouldn't be able to skip their turn by trying a move that doesn't change the board.
   Check for a  win condition.
   Check for a lose condition.


using System;
 
namespace g2048_csharp
{
    internal class Tile
    {
        public Tile()
        {
            Value = 0;
            IsBlocked = false;
        }
 
        public int Value { get; set; }
        public bool IsBlocked { get; set; }
    }
 
    internal enum MoveDirection
    {
        Up,
        Down,
        Left,
        Right
    }
 
    internal class G2048
    {
        public G2048()
        {
            _isDone = false;
            _isWon = false;
            _isMoved = true;
            _score = 0;
            InitializeBoard();
        }
 
        private void InitializeBoard()
        {
            for (int y = 0; y < 4; y++)
            {
                for (int x = 0; x < 4; x++)
                {
                    _board[x, y] = new Tile();
                }
            }
        }
 
        private bool _isDone;
        private bool _isWon;
        private bool _isMoved;
        private int _score;
        private readonly Tile[,] _board = new Tile[4, 4];
        private readonly Random _rand = new Random();
 
        public void Loop()
        {
            AddTile();
            while (true)
            {
                if (_isMoved)
                {
                    AddTile();
                }
 
                DrawBoard();
                if (_isDone)
                {
                    break;
                }
 
                WaitKey();
            }
 
            string endMessage = _isWon ? "You've made it!" : "Game Over!";
            Console.WriteLine(endMessage);
        }
 
        private void DrawBoard()
        {
            Console.Clear();
            Console.WriteLine("Score: " + _score + "\n");
            for (int y = 0; y < 4; y++)
            {
                Console.WriteLine("+------+------+------+------+");
                Console.Write("| ");
                for (int x = 0; x < 4; x++)
                {
                    if (_board[x, y].Value == 0)
                    {
                        const string empty = " ";
                        Console.Write(empty.PadRight(4));
                    }
                    else
                    {
                        Console.Write(_board[x, y].Value.ToString().PadRight(4));
                    }
 
                    Console.Write(" | ");
                }
 
                Console.WriteLine();
            }
 
            Console.WriteLine("+------+------+------+------+\n\n");
        }
 
        private void WaitKey()
        {
            _isMoved = false;
            Console.WriteLine("(W) Up (S) Down (A) Left (D) Right");
            char input;
            char.TryParse(Console.ReadKey().Key.ToString(), out input);
 
            switch (input)
            {
                case 'W':
                    Move(MoveDirection.Up);
                    break;
                case 'A':
                    Move(MoveDirection.Left);
                    break;
                case 'S':
                    Move(MoveDirection.Down);
                    break;
                case 'D':
                    Move(MoveDirection.Right);
                    break;
            }
 
            for (int y = 0; y < 4; y++)
            {
                for (int x = 0; x < 4; x++)
                {
                    _board[x, y].IsBlocked = false;
                }
            }
        }
 
        private void AddTile()
        {
            for (int y = 0; y < 4; y++)
            {
                for (int x = 0; x < 4; x++)
                {
                    if (_board[x, y].Value != 0) continue;
                    int a, b;
                    do
                    {
                        a = _rand.Next(0, 4);
                        b = _rand.Next(0, 4);
                    } while (_board[a, b].Value != 0);
 
                    double r = _rand.NextDouble();
                    _board[a, b].Value = r > 0.89f ? 4 : 2;
 
                    if (CanMove())
                    {
                        return;
                    }
                }
            }
 
            _isDone = true;
        }
 
        private bool CanMove()
        {
            for (int y = 0; y < 4; y++)
            {
                for (int x = 0; x < 4; x++)
                {
                    if (_board[x, y].Value == 0)
                    {
                        return true;
                    }
                }
            }
 
            for (int y = 0; y < 4; y++)
            {
                for (int x = 0; x < 4; x++)
                {
                    if (TestAdd(x + 1, y, _board[x, y].Value)
                        || TestAdd(x - 1, y, _board[x, y].Value)
                        || TestAdd(x, y + 1, _board[x, y].Value)
                        || TestAdd(x, y - 1, _board[x, y].Value))
                    {
                        return true;
                    }
                }
            }
 
            return false;
        }
 
        private bool TestAdd(int x, int y, int value)
        {
            if (x < 0 || x > 3 || y < 0 || y > 3)
            {
                return false;
            }
 
            return _board[x, y].Value == value;
        }
 
        private void MoveVertically(int x, int y, int d)
        {
            if (_board[x, y + d].Value != 0
                && _board[x, y + d].Value == _board[x, y].Value
                && !_board[x, y].IsBlocked
                && !_board[x, y + d].IsBlocked)
            {
                _board[x, y].Value = 0;
                _board[x, y + d].Value *= 2;
                _score += _board[x, y + d].Value;
                _board[x, y + d].IsBlocked = true;
                _isMoved = true;
            }
            else if (_board[x, y + d].Value == 0
                     && _board[x, y].Value != 0)
            {
                _board[x, y + d].Value = _board[x, y].Value;
                _board[x, y].Value = 0;
                _isMoved = true;
            }
 
            if (d > 0)
            {
                if (y + d < 3)
                {
                    MoveVertically(x, y + d, 1);
                }
            }
            else
            {
                if (y + d > 0)
                {
                    MoveVertically(x, y + d, -1);
                }
            }
        }
 
        private void MoveHorizontally(int x, int y, int d)
        {
            if (_board[x + d, y].Value != 0
                && _board[x + d, y].Value == _board[x, y].Value
                && !_board[x + d, y].IsBlocked
                && !_board[x, y].IsBlocked)
            {
                _board[x, y].Value = 0;
                _board[x + d, y].Value *= 2;
                _score += _board[x + d, y].Value;
                _board[x + d, y].IsBlocked = true;
                _isMoved = true;
            }
            else if (_board[x + d, y].Value == 0
                     && _board[x, y].Value != 0)
            {
                _board[x + d, y].Value = _board[x, y].Value;
                _board[x, y].Value = 0;
                _isMoved = true;
            }
 
            if (d > 0)
            {
                if (x + d < 3)
                {
                    MoveHorizontally(x + d, y, 1);
                }
            }
            else
            {
                if (x + d > 0)
                {
                    MoveHorizontally(x + d, y, -1);
                }
            }
        }
 
        private void Move(MoveDirection direction)
        {
            switch (direction)
            {
                case MoveDirection.Up:
                    for (int x = 0; x < 4; x++)
                    {
                        int y = 1;
                        while (y < 4)
                        {
                            if (_board[x, y].Value != 0)
                            {
                                MoveVertically(x, y, -1);
                            }
 
                            y++;
                        }
                    }
 
                    break;
                case MoveDirection.Down:
                    for (int x = 0; x < 4; x++)
                    {
                        int y = 2;
                        while (y >= 0)
                        {
                            if (_board[x, y].Value != 0)
                            {
                                MoveVertically(x, y, 1);
                            }
 
                            y--;
                        }
                    }
 
                    break;
                case MoveDirection.Left:
                    for (int y = 0; y < 4; y++)
                    {
                        int x = 1;
                        while (x < 4)
                        {
                            if (_board[x, y].Value != 0)
                            {
                                MoveHorizontally(x, y, -1);
                            }
 
                            x++;
                        }
                    }
 
                    break;
                case MoveDirection.Right:
                    for (int y = 0; y < 4; y++)
                    {
                        int x = 2;
                        while (x >= 0)
                        {
                            if (_board[x, y].Value != 0)
                            {
                                MoveHorizontally(x, y, 1);
                            }
 
                            x--;
                        }
                    }
 
                    break;
            }
        }
    }
 
    internal static class Program
    {
        public static void Main(string[] args)
        {
            RunGame();
        }
 
        private static void RunGame()
        {
            G2048 game = new G2048();
            game.Loop();
 
            CheckRestart();
        }
 
        private static void CheckRestart()
        {
            Console.WriteLine("(N) New game (P) Exit");
            while (true)
            {
                char input;
                char.TryParse(Console.ReadKey().Key.ToString(), out input);
                switch (input)
                {
                    case 'N':
                        RunGame();
                        break;
                    case 'P':
                        return;
                    default:
                        ClearLastLine();
                        break;
                }
            }
        }
 
        private static void ClearLastLine()
        {
            Console.SetCursorPosition(0, Console.CursorTop);
            Console.Write(new string(' ', Console.BufferWidth));
            Console.SetCursorPosition(0, Console.CursorTop - 1);
        }
    }
}
 