我设计了一个游戏,试图使用坚实的原则。我应该做些什么来使程序更具可扩展性和可读性。提前谢谢。
Cell类
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类
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类
public class Map
{
public List 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(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 NotOpenedSquares()
{
IEnumerable notOpenedSquares;
notOpenedSquares = AllSquares
.Cast()
.Where(item => !item.IsMine & !item.IsUncovered);
return notOpenedSquares;
}
// create a mine-free region 3X3 or 2X2 at corners for first click event
IEnumerable MineFreeRegion(Point firstClickedSquare)
{
int x = firstClickedSquare.X;
int y = firstClickedSquare.Y;
List neighborhoods = NeighborhoodCells(firstClickedSquare).ToList();
neighborhoods.Add(AllSquares[y, x]);
return neighborhoods;
}
// getting list of adjacent neighborhood squares
IEnumerable NeighborhoodCells(Point square)
{
var adjacentCells = new List();
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 MinesFromCloseToAway(Point clicked)
{
IEnumerable 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 clicked)
{
clicked.IsUncovered = true;
squares.Add(clicked);
IEnumerable 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 mineFreeRegion = MineFreeRegion(firstClickedSquare);
AllSquares
.Cast()
.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类
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 NotOpenedSquare()
{
IEnumerable 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 MinesToShow()
{
IEnumerable minesToShow = map.SquaresWithMine;
return minesToShow;
}
// getting list of in line mines for exploding all if a mine is clicked by the user
public IEnumerable MinesToExplode(Squares clicked)
{
IEnumerable 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 SquaresWillBeOpened(Squares clicked)
{
var squaresWillBeOpened = new List();
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类
public partial class Form1 : Form
{
Color buttonBackColor;
int buttonSize;
int buttonNumberX;
int buttonNumberY;
int mineNumber;
Game game;
Timer timer;
ElapsedTime elapsedTime;
Button[,] allButtons;
Dictionary squaresInButtons;
Dictionary gameResultText;
Dictionary gameResultColor;
public Form1()
{
buttonSize = 35;
buttonNumberX = 38;
buttonNumberY = 17;
mineNumber = (buttonNumberX * buttonNumberY) / 9 ;
buttonBackColor = Color.FromArgb(160, 90, 250);
gameResultText = new Dictionary
{
{ GameStatus.Won, "- - - - - WON - - - - - -" },
{ GameStatus.Lost, "- - - - - LOST - - - - - -" }
};
gameResultColor = new Dictionary
{
{ 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();
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 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 allMines = game.MinesToShow();
ShowMines(allMines);
Thread.Sleep(1000);
// put exploded mine image on every mine
//in order to their distance first clicked mine
IEnumerable 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 notDetonetedMines = game.MinesToShow();
ShowMines(notDetonetedMines);
}
else
{
// opening all not opened non-mine squares after all mines exploded
IEnumerable 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 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 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];
}
}发布于 2020-03-26 12:41:28
要复习的代码太多了。我给了它一个粗略的回顾,并有一些评论,但这绝不是一个详尽的审查。
总的来说,我喜欢你的风格。不错的缩进,大括号的使用,类访问修饰符,IEnumerable(s),和变量名没有缩写。大多数时候,但不是全部,你的名字都很清楚。
其他的事情很突出。除非枚举使用标志,否则命名应该是单数,而不是复数。有关更多信息,请参见Enum设计。这样做可能会导致一些混乱,现在将枚举重命名为Click或Action。也许您可以将它们重命名为CellClick和CellAction。
我不是Squares这个类名的粉丝。我个人会选择Cell。这是品味的问题。但是,如果将它保持为正方形,那么这个类名也应该是单数Square,因为该类描述的是单个正方形,而不是集合。
我不禁想知道,IsMine、IsFlagged和IsUncovered的平方属性是否不能浓缩成一个国家。也许不是所有的三个都可以使用,但两个也许可以决定一个国家。
我认为没有理由使用ElapsedTime类。TimeInHourFormat可以成为一个扩展方法,或者仅仅是一个放置在UI表单中的方法。如果它是一个扩展方法,可能会有两个过载。一个接受(this Stopwatch stopwatch),另一个有(this TimeSpan elapsedTime)。主要的逻辑是TimeSpan。
这是我上班前所能做的。总的来说,它看起来很好,但肯定还有改进的余地。
具有扩展的<#>UPDATE
我有一些空闲时间抽出一些扩展示例。请注意,ToString("00")以更少的代码处理nit-挑剔的细节。
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}";
}
}别被"00“扔了几个小时。它将显示至少2位数,但如果小时数> 99,则显示所有数字。您已经从担心让您的自定义秒表班负责运行秒表的所有机制中解脱出来了。您所关心的只是格式化经过的时间,不只是针对Diagnostics.Stopwatch,而是对任何TimeSpan。
https://codereview.stackexchange.com/questions/239207
复制相似问题