4
\$\begingroup\$

I designed a game with trying to use solid principles. What changes should I do for making program more extensible, readable. Thanks in advance.

Cell class

public class Squares
{
    public int NumberOfAdjacentMines { get; set; }
    public bool IsMine { get; set; }
    public bool IsFlagged { get; set; }
    // show if square is left-clicked and opened
    public bool IsUncovered { get; set; }
    public Point Location { get ; set ; }    
}

ElapsedTime Class

public class ElapsedTime
{
    Stopwatch stopwatch;
    long elapsedTime;

    public ElapsedTime()
    {
        elapsedTime = 0;
        stopwatch = new Stopwatch();
        stopwatch.Start();
    }

    // getting elapsed time in watch format like 01 : 57
    public string TimeInHourFormat()
    {          
        elapsedTime = stopwatch.ElapsedMilliseconds / 1000;
        int second = (int) elapsedTime % 60;
        int minute = (int) elapsedTime / 60;
        string result = string.Empty;

        if (second < 10)
        {
            result = $": 0{second}";
        }
        else  
        {
            result = $": {second}";
        }

        if (minute < 10)
        {                
            result = $"0{minute} {result}";
        }
        else  
        {
            result = $"{minute} {result}";
        }

        if (elapsedTime > 3600)
        {
            int hour = (int)elapsedTime / 3600;
            result = $"{hour} : {result}";
        }

        return result;
    }

    public void StopTimer()
    {
        stopwatch.Stop();
    }

}

Map Class

public class Map
{

    public List<Squares> SquaresWithMine { get; private set; }
    public Squares[,] AllSquares { get; set; }
    public int MineNumber { get; set; }

    public Map(int squaresHorizontal, int squaresVertical, int mineNumber)
    {
        MineNumber = mineNumber;
        AllSquares = new Squares[squaresVertical, squaresHorizontal];
        SquaresWithMine = new List<Squares>(mineNumber);


        for (int i = 0; i < AllSquares.GetLength(0); i++)
        {
            for (int j = 0; j < AllSquares.GetLength(1); j++)
            {
                AllSquares[i, j] = new Squares()
                {
                    Location = new Point(j, i),
                    IsFlagged = false,
                    IsMine = false,
                    IsUncovered = false,
                    NumberOfAdjacentMines = 0
                };                   
            }
        }
    }

    public Squares Square(int x, int y)
    {
        return AllSquares[y, x];
    }

    // list of non-clicked and no mine squares For showing left squares
    // after all mines are opened
    public IEnumerable<Squares> NotOpenedSquares()
    {
        IEnumerable<Squares> notOpenedSquares;

        notOpenedSquares = AllSquares
                             .Cast<Squares>()
                               .Where(item => !item.IsMine & !item.IsUncovered);

        return notOpenedSquares;
    }

    // create a mine-free region 3X3 or 2X2 at corners for first click event
    IEnumerable<Squares> MineFreeRegion(Point firstClickedSquare)
    {
        int x = firstClickedSquare.X;
        int y = firstClickedSquare.Y;

        List<Squares> neighborhoods = NeighborhoodCells(firstClickedSquare).ToList();
        neighborhoods.Add(AllSquares[y, x]); 

        return neighborhoods;
    }

    // getting list of adjacent neighborhood squares 
    IEnumerable<Squares> NeighborhoodCells(Point square)
    {
        var adjacentCells = new List<Squares>(); 
        int currentTop;
        int currentLeft;

        for (int i = -1; i < 2; i++)
        {
            for (int j = -1; j < 2; j++)
            {
                if (i != 0 | j != 0)
                {
                    currentLeft = square.X + j;
                    currentTop = square.Y + i;

                    if (currentLeft > -1 & currentLeft < AllSquares.GetLength(1))
                    {
                        if (currentTop > -1 & currentTop < AllSquares.GetLength(0))
                        {
                            Squares neighborhood = AllSquares[currentTop, currentLeft];
                            adjacentCells.Add(neighborhood); 
                        }
                    }
                }
            }
        }

        return adjacentCells;
    }

    // getting mine list in the order on being close to first clicked mine
    public IEnumerable<Squares> MinesFromCloseToAway(Point clicked)
    {
        IEnumerable<Squares> orderedMines;
        orderedMines = SquaresWithMine
                       .OrderBy(item => MineDistanceToExplosion(item.Location, clicked));

        return orderedMines;
    }

    // calculate mines distance to first clicked mine
    int MineDistanceToExplosion(Point mine, Point explosion)
    {
        int x = mine.X - explosion.X;
        int y = mine.Y - explosion.Y;
        int distance = x * x + y * y;

        return distance;
    }

    // if a square that has no mine neighborhood is clicked, then it and its adjacent cells
    // will be added to list for opening all once
    public void OpenSquaresRecursively(IList<Squares> squares, Squares clicked)
    {
        clicked.IsUncovered = true;
        squares.Add(clicked);
        IEnumerable<Squares> nghbrhds = NeighborhoodCells(clicked.Location);

        foreach (Squares neighborhoodSquare in nghbrhds)
        {
            if (neighborhoodSquare.IsUncovered | neighborhoodSquare.IsFlagged)
            {
                continue;
            }

            if (neighborhoodSquare.NumberOfAdjacentMines == 0)
            {
                OpenSquaresRecursively(squares, neighborhoodSquare);                    
            }
            else
            {
                neighborhoodSquare.IsUncovered = true;
                squares.Add(neighborhoodSquare);
            }
        }
    }

    public void OpenSquare(Squares square)
    {
        square.IsUncovered = true;
    }

    public void ChangeFlagState(Squares clicked)
    {
        clicked.IsFlagged = !clicked.IsFlagged;
    }

    // when first click is made, a mine free region that include first clicked square
    // in the middle is created and those cells is removed from the all cell list 
    // for placing mines in squares those left. after that this in line list is shuffled
    // with creating random numbers 
    public void LocateMinesRandomly(Point firstClickedSquare)
    {
        Random random = new Random();
        IEnumerable<Squares> mineFreeRegion = MineFreeRegion(firstClickedSquare);

        AllSquares
          .Cast<Squares>()
              .Where(point => !mineFreeRegion.Any(square => square.Location == point.Location))
                .OrderBy(item => random.Next())
                   .Take(MineNumber)
                      .ToList()
                         .ForEach(item =>
                         {
                             Squares mine = AllSquares[item.Location.Y, item.Location.X];
                             mine.IsMine = true;
                             SquaresWithMine.Add(mine);
                         });
    }

    // calculate number of adjacent mines that a no-mine square has 
    public void FindMinesAdjacent()
    {
        SquaresWithMine
          .SelectMany(item
            => NeighborhoodCells(item.Location))
              .ToList()
                .ForEach(item => item.NumberOfAdjacentMines++);
    }



}

Game class

public enum Clicks
{
    DefaultClick = 0,
    LeftClick = 1,
    RightClick = 2
}

public enum Actions
{
    DoNothing = 0,
    PutFlag,
    RemoveFlag,
    ExplodeAllMines,
    OpenSquare,
    OpenSquaresRecursively
}

public enum GameStatus
{
    Default = 0,
    NotFinished,
    Won,
    Lost
}

public class Game
{
    bool firstClick;
    bool clickedToMine;
    public int WinningScore { get; private set; }
    public int CurrentScore { get; private set; }
    public Squares[,] Squares { get; private set; }
    Map map;

    public Game(int squaresHorizontal, int squaresVertical, int mineNumber)
    {
        CurrentScore = 0;
        WinningScore = squaresHorizontal * squaresVertical - mineNumber;
        firstClick = false;
        clickedToMine = false;
        map = new Map(squaresHorizontal, squaresVertical, mineNumber);
        Squares = map.AllSquares;
    }

    public int NumberOfNotOpenedSafetySquare()
    {
        return WinningScore - CurrentScore;
    }

    public Squares Square(int x, int y)
    {
        return map.AllSquares[y, x];
    }


    public GameStatus GameSituation()
    {
        if (CurrentScore == WinningScore)
        {
            return GameStatus.Won;
        }
        else if (clickedToMine)
        {
            return GameStatus.Lost;
        }
        else
        {
            return GameStatus.NotFinished;
        }
    }

    public Actions ClickSquare(Clicks mouseClick, Squares clicked)
    {
        // running once when map is first time clicked by left click during game 
        if (!firstClick & mouseClick == Clicks.LeftClick)
        {
            StartGame(clicked.Location);
            firstClick = !firstClick;
        }

        if (mouseClick == Clicks.RightClick)
        {
            Actions result;

            // if a square ic left-clicked before then right click has no effect
            if (clicked.IsUncovered)
            {
                return Actions.DoNothing;
            }

            // if square has flag on it then it will be removed
            // else flag will be placed on it
            if (clicked.IsFlagged)
            {
                result = Actions.RemoveFlag;
            }
            else 
            {
                result = Actions.PutFlag;
            }


            ChangeFlagState( clicked);

            return result;

        }

        if (mouseClick == Clicks.LeftClick)
        {
            // if a square that has flag on it received left-click 
            // there will be no effect
            if (clicked.IsFlagged)
            {
                return Actions.DoNothing;
            }

            if (clicked.IsMine)
            {
                clickedToMine = true;
                return Actions.ExplodeAllMines;
            }
            // if a square that has mine neighborhood is clicked, 
            // a number of mines will be wrote on it 
            if (clicked.NumberOfAdjacentMines > 0)
            {
                OpenSquare(clicked);
                return Actions.OpenSquare;
            }
            // if a square that has no mine neighborhood is clicked, 
            // then its neighborhodo and itself will be opened at once
            else
            {
                return Actions.OpenSquaresRecursively;
            }
        }

        return Actions.DoNothing;
    }


    public IEnumerable<Squares> NotOpenedSquare()
    {
        IEnumerable<Squares> notOpenedSquares = map.NotOpenedSquares();

        return notOpenedSquares;
    }

    // getting list of mines for showing where each all of them
    //  if a mine is clicked by the user
    public IEnumerable<Squares> MinesToShow()
    {
        IEnumerable<Squares> minesToShow = map.SquaresWithMine;

        return minesToShow;
    }

    // getting list of in line mines for exploding all if a mine is clicked by the user
    public IEnumerable<Squares> MinesToExplode(Squares clicked)
    {
        IEnumerable<Squares> minesToExplode = map.MinesFromCloseToAway(clicked.Location);

        return minesToExplode; 
    }

    // if a square that has no mine neighborhood is clicked then 
    // its all neighborhoods and itself is added a list for opening all in once
    // and number of those added to score
    public IEnumerable<Squares> SquaresWillBeOpened(Squares clicked)
    {           
        var squaresWillBeOpened = new List<Squares>();
        map.OpenSquaresRecursively(squaresWillBeOpened, clicked);
        CurrentScore += squaresWillBeOpened.Count;

        return squaresWillBeOpened;
    }

    public void StartGame(Point firstClickedSquare)
    {
        map.LocateMinesRandomly(firstClickedSquare);
        map.FindMinesAdjacent();
    }

    public void OpenSquare(Squares square)
    {
        map.OpenSquare(square);
        CurrentScore++;
    }

    void ChangeFlagState(Squares clicked)
    {
        map.ChangeFlagState(clicked);
    }
}

Form Class

public partial class Form1 : Form
{

    Color buttonBackColor;
    int buttonSize;
    int buttonNumberX;
    int buttonNumberY;
    int mineNumber;
    Game game;
    Timer timer;
    ElapsedTime elapsedTime;
    Button[,] allButtons;
    Dictionary<Button, Squares> squaresInButtons;
    Dictionary<GameStatus, string> gameResultText;
    Dictionary<GameStatus, Color> gameResultColor;

    public Form1()
    {
        buttonSize = 35;
        buttonNumberX = 38;
        buttonNumberY = 17;
        mineNumber = (buttonNumberX * buttonNumberY) / 9 ;
        buttonBackColor = Color.FromArgb(160, 90, 250);

        gameResultText = new Dictionary<GameStatus, string>
        {
            { GameStatus.Won, "- - - - - WON - - - - - -" },
            { GameStatus.Lost, "- - - - - LOST - - - - - -" }
        };

        gameResultColor = new Dictionary<GameStatus, Color>
        {
            { GameStatus.Won, Color.Green },
            { GameStatus.Lost, Color.Red }
        };

        InitializeComponent();           
    }

    private void Form1_Load(object sender, EventArgs e)
    {           
        int panelWidth = buttonNumberX * buttonSize;
        int panelHeight = buttonNumberY * buttonSize;
        this.Width = panelWidth + 50;
        this.Height = panelHeight + 100;

        pnlMayınlar.Size = new Size(panelWidth, panelHeight);
        pnlMayınlar.Left = 20;
        pnlMayınlar.Top = 85;
        pnlMayınlar.BackColor = Color.Black;

        InitializeGame();

        int lblTop = 40;
        label2.Top = lblTop;           
        lblTimeShower.Top = lblTop;

        label1.Text = "Remaining Square : " + game.NumberOfNotOpenedSafetySquare().ToString();
        label1.Location = new Point(panelWidth - label1.Width, lblTop);
        pnlMayınlar.Show();
    }


    void InitializeGame()
    {
        squaresInButtons = new Dictionary<Button, Squares>();
        game = new Game(buttonNumberX, buttonNumberY, mineNumber);
        allButtons = new Button[buttonNumberY, buttonNumberX];
        pnlMayınlar.Enabled = true;

        for (int i = 0; i < game.Squares.GetLength(0); i++)
        {
            for (int j = 0; j < game.Squares.GetLength(1); j++)
            {
                Button button = CreateButton(j, i);
                squaresInButtons.Add(button, game.Square(j, i));
                pnlMayınlar.Controls.Add(button);
            }
        }

        label2.Hide();
        label1.Show();
        SetLabelText(game.NumberOfNotOpenedSafetySquare());
        elapsedTime = new ElapsedTime();
        timer = new Timer
        {
            Interval = 1000,

        };
        timer.Tick += DrawElapsedTime;
        timer.Start();
    }

    Button CreateButton(int x, int y)
    {
                Button button = new Button()
                {
                    Size = new Size(buttonSize, buttonSize),
                    Top = y * buttonSize,
                    Left = x * buttonSize,
                    BackColor = buttonBackColor,
                    BackgroundImageLayout = ImageLayout.Stretch
                };
                button.MouseDown += ClickingOnSquares;
                allButtons[y, x] = button;

                return button;
    }

    private void yeniOyunToolStripMenuItem_Click(object sender, EventArgs e)
    {
        pnlMayınlar.Controls.Clear();
        InitializeGame();
    }


    void ClickingOnSquares(object sender, MouseEventArgs e)
    {
        Button clicked = sender as Button;
        Squares square = squaresInButtons[clicked];

        if (e.Button == MouseButtons.Right)
        {

            Actions actions = game.ClickSquare(Clicks.RightClick, square);

            if (actions == Actions.DoNothing)
            {
                return;
            }

            if (actions == Actions.PutFlag)
            {
                clicked.BackgroundImage = Properties.Resources.flagIcon;
            }
            else if(actions == Actions.RemoveFlag)
            {
                clicked.BackgroundImage = null;
                clicked.BackColor = buttonBackColor;
            }                
        }

        if (e.Button == MouseButtons.Left)
        {
            Actions actions = game.ClickSquare(Clicks.LeftClick, square);

            if (actions == Actions.DoNothing)
            {
                return;
            }
            // open left clicked square that has at least one neighborhood mine
            else if (actions == Actions.OpenSquare)
            {
                OpenMineFreeSquare(square);
            }
            // open square that has no mine neighborhood and its neighborhoods at once
            else if (actions == Actions.OpenSquaresRecursively)
            {
                IEnumerable<Squares> squareList = game.SquaresWillBeOpened(square);
                foreach (Squares item in squareList)
                {
                    OpenMineFreeSquare(item);
                }
            }
            else if (actions == Actions.ExplodeAllMines)
            {
                // show where all mines are after any mine is clicked
                IEnumerable<Squares> allMines = game.MinesToShow();
                ShowMines(allMines);
                Thread.Sleep(1000);
                // put exploded mine image on every mine 
                //in order to their distance first clicked mine
                IEnumerable<Squares> inLineMines = game.MinesToExplode(square);
                ExplodeAllMines(inLineMines);
            }


            SetLabelText(game.NumberOfNotOpenedSafetySquare());

            // getting game situation for checking if there is a win or lose
            GameStatus gameState = game.GameSituation();

            // if game should be continue then leave method 
            // else check there is a win or lose and do necessary things
            if (gameState == GameStatus.NotFinished | gameState == GameStatus.Default)
            {
                return;
            }
            else
            {
                // stop counting time and write resulting text above map
                timer.Stop();
                label1.Hide();

                label2.Show();
                label2.ForeColor = gameResultColor[gameState];
                label2.Text = gameResultText[gameState];
                label2.Left = (this.Width - label2.Width) / 2;

                if (gameState == GameStatus.Won)
                {
                    IEnumerable<Squares> notDetonetedMines = game.MinesToShow();
                    ShowMines(notDetonetedMines);
                }
                else
                {
                    // opening all not opened non-mine squares after all mines exploded
                    IEnumerable<Squares> NotOpenedSquares = game.NotOpenedSquare();
                    foreach (Squares item in NotOpenedSquares)
                    {
                        OpenMineFreeSquare(item);
                        Thread.Sleep(10);
                    }
                }

                pnlMayınlar.Enabled = false;
            }    
        }  

    }

    // when a no-mine square is clicked, number of neighborhood mine is wrote 
    // on it and colored depending on that number
    void OpenMineFreeSquare(Squares square)
    {
        Button clicked = allButtons[square.Location.Y, square.Location.X];
        if (square.NumberOfAdjacentMines > 0)
        {
            clicked.Text = square.NumberOfAdjacentMines.ToString();
        }           
        clicked.BackColor = SquareTextColor(square.NumberOfAdjacentMines);
        clicked.Enabled = false;
    }

    // put a detoneted mine image on squares after any mine is clicked
    void ExplodeAllMines(IEnumerable<Squares> inLineMines)
    {
        foreach (Squares item in inLineMines)
        {
            Button willBeDetoneted = allButtons[item.Location.Y, item.Location.X];
            willBeDetoneted.BackgroundImage = Properties.Resources.detonetedmine;
            willBeDetoneted.Enabled = false;
            willBeDetoneted.Update();
            Thread.Sleep(50);
        }  
    }

    // put a not-detoneted mine image on squares before detoneted mine image is put
    // for making exploding animation
    void ShowMines(IEnumerable<Squares> inLineMines)
    {
        foreach (Squares item in inLineMines)
        {
            Button willBeDetoneted = allButtons[item.Location.Y, item.Location.X];
            willBeDetoneted.BackgroundImage = Properties.Resources.notDetonetedMine;
            willBeDetoneted.Enabled = false;
        }
    }

    // start when map is loaded and showing elapsed time at the left upper corner 
    void DrawElapsedTime(object source, EventArgs e)
    {
        lblTimeShower.Text = elapsedTime.TimeInHourFormat();
    }

    // write number of how many more square must be clicked for to win
    void SetLabelText(int score)
    {
        label1.Text = "Remaining Square : " + score.ToString();
    }

    // color list for squares those have neighborhood mine at least one
    Color SquareTextColor(int mineNumber)
    {
        Color[] colors = {
             Color.FromArgb(180, 180, 180) ,
             Color.FromArgb(20, 110, 250) ,
             Color.FromArgb(10, 220, 20),
             Color.FromArgb(250, 20, 20),
             Color.FromArgb(150, 20, 60),
             Color.FromArgb(180, 40, 170),
             Color.FromArgb(90, 20, 20),
             Color.FromArgb(80, 30, 60),
             Color.FromArgb(50, 10, 40)
        };

        return colors[mineNumber];
    }
}
\$\endgroup\$
2
\$\begingroup\$

That's a lot of code to review. I've given it a cursory review and have a few comments, but by no means is it an exhaustive review.

Overall, I like your style. Nice indentation, uses of braces, class access modifiers, IEnumerable(s), and variable names not being abbreviated. Most of the times, but not all, your names are clear.

Other things stand out. Unless an enum uses flags, naming should be singular, not plural. See Enum Design for more. Doing so could cause some confusion with an enum now renamed to Click or Action. Perhaps you may rename them to CellClick and CellAction.

I'm not a fan of the class name Squares. I personally would have chosen Cell. This is a matter of taste. However, if you keep it as square, then that class name should also be singular Square since the class describes a single square and not a collection.

I can't help but wonder if the Square properties IsMine, IsFlagged, and IsUncovered could not be condensed into a State. Maybe not all 3 could be used, but 2 perhaps could determine a State.

I see no reason for the ElapsedTime class. The TimeInHourFormat could become an extension method or just a method placed inside the UI form. If it were an extension method, there could be 2 overloads. One that accepts (this Stopwatch stopwatch) and the other that has (this TimeSpan elapsedTime). The main logic would be to the TimeSpan.

That's about all I can cover before getting to work. Overall it looks very good, but there is definitely room for improvement.

UPDATE WITH EXTENSIONS

I had some free time to whip out sample extensions. Note that ToString("00") takes care of the nit-picky details in a lot less code.

public static class StopwatchExtensions
{
    public static string ToHoursMinutesSeconds(this Stopwatch stopwatch) => ToHoursMinutesSeconds(stopwatch.Elapsed);

    public static string ToHoursMinutesSeconds(this TimeSpan elapsed) 
    {
        // You really do not need secondsText and minutesText.  
        // You could use each respective right-hand expression in the return statement below.
        var secondsText = elapsed.Seconds.ToString("00");
        var minutesText = elapsed.Minutes.ToString("00");
        var hours = (long)Math.Truncate(elapsed.TotalHours);
        var hoursText = (hours > 0) ? hours.ToString("00") + ":" : "";
        return $"{hoursText}{minutesText}:{secondsText}";
    }
}

Don't be thrown off by "00" for hours. It will display a minimum of 2 digits, but if hours is > 99, it will display all the digits. You are liberated from worrying about having your custom stopwatch class take care of all the mechanics of operating a stopwatch. All you care about is formatting the elapsed time, not just for a Diagnostics.Stopwatch but for any TimeSpan.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ @yuga Answer updated with sample code \$\endgroup\$ – Rick Davin Apr 3 at 12:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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