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];
}
}