310 lines
8.3 KiB
C#
310 lines
8.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace Minesweeper.Core
|
|
{
|
|
public sealed class BoardService : IBoardService
|
|
{
|
|
private readonly IGameSettingsService settingsService;
|
|
private readonly Random random = new Random();
|
|
private CellData[,] cells;
|
|
|
|
public BoardService(IGameSettingsService settingsService)
|
|
{
|
|
this.settingsService = settingsService;
|
|
}
|
|
|
|
public int Width { get; private set; }
|
|
public int Height { get; private set; }
|
|
public int MinesCount { get; private set; }
|
|
public bool IsGenerated { get; private set; }
|
|
public int OpenedSafeCellsCount { get; private set; }
|
|
public int SafeCellsCount => Width * Height - MinesCount;
|
|
|
|
public void InitializeEmptyBoard()
|
|
{
|
|
Width = settingsService.SizeX;
|
|
Height = settingsService.SizeY;
|
|
MinesCount = Math.Min(settingsService.MinesCount, Width * Height - 1);
|
|
OpenedSafeCellsCount = 0;
|
|
IsGenerated = false;
|
|
cells = new CellData[Width, Height];
|
|
|
|
for (var x = 0; x < Width; x++)
|
|
{
|
|
for (var y = 0; y < Height; y++)
|
|
{
|
|
cells[x, y] = new CellData(x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool GenerateAfterFirstClick(int safeX, int safeY)
|
|
{
|
|
EnsureInitialized();
|
|
|
|
if (IsGenerated || !IsInside(safeX, safeY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
PlaceMines(safeX, safeY);
|
|
CalculateNeighborMines();
|
|
|
|
IsGenerated = true;
|
|
return true;
|
|
}
|
|
|
|
public BoardActionResult OpenCell(int x, int y)
|
|
{
|
|
EnsureInitialized();
|
|
|
|
if (!IsGenerated || !IsInside(x, y))
|
|
{
|
|
return BoardActionResult.InvalidAction;
|
|
}
|
|
|
|
var cell = cells[x, y];
|
|
if (cell.IsOpened || cell.IsFlagged)
|
|
{
|
|
return BoardActionResult.NoChange;
|
|
}
|
|
|
|
if (cell.IsMine)
|
|
{
|
|
cell.IsOpened = true;
|
|
return new BoardActionResult(true, true, false, false);
|
|
}
|
|
|
|
if (cell.NeighborMines == 0)
|
|
{
|
|
RevealEmptyArea(x, y);
|
|
}
|
|
else
|
|
{
|
|
OpenSafeCell(cell);
|
|
}
|
|
|
|
return new BoardActionResult(true, false, IsWin(), false);
|
|
}
|
|
|
|
public BoardActionResult ToggleFlag(int x, int y)
|
|
{
|
|
EnsureInitialized();
|
|
|
|
if (!IsInside(x, y))
|
|
{
|
|
return BoardActionResult.InvalidAction;
|
|
}
|
|
|
|
var cell = cells[x, y];
|
|
if (cell.IsOpened)
|
|
{
|
|
return BoardActionResult.NoChange;
|
|
}
|
|
|
|
cell.IsFlagged = !cell.IsFlagged;
|
|
return new BoardActionResult(true, false, false, false);
|
|
}
|
|
|
|
public bool IsInside(int x, int y)
|
|
{
|
|
return cells != null && x >= 0 && y >= 0 && x < Width && y < Height;
|
|
}
|
|
|
|
public bool TryGetCell(int x, int y, out BoardCellData cell)
|
|
{
|
|
if (!IsInside(x, y))
|
|
{
|
|
cell = default;
|
|
return false;
|
|
}
|
|
|
|
cell = ToData(cells[x, y]);
|
|
return true;
|
|
}
|
|
|
|
public IReadOnlyList<BoardCellData> GetCells()
|
|
{
|
|
EnsureInitialized();
|
|
|
|
var result = new List<BoardCellData>(Width * Height);
|
|
for (var y = 0; y < Height; y++)
|
|
{
|
|
for (var x = 0; x < Width; x++)
|
|
{
|
|
result.Add(ToData(cells[x, y]));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void EnsureInitialized()
|
|
{
|
|
if (cells == null)
|
|
{
|
|
InitializeEmptyBoard();
|
|
}
|
|
}
|
|
|
|
private void PlaceMines(int safeX, int safeY)
|
|
{
|
|
var positions = new List<int>(Width * Height - 1);
|
|
for (var x = 0; x < Width; x++)
|
|
{
|
|
for (var y = 0; y < Height; y++)
|
|
{
|
|
if (x == safeX && y == safeY)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
positions.Add(ToIndex(x, y));
|
|
}
|
|
}
|
|
|
|
Shuffle(positions);
|
|
|
|
for (var i = 0; i < MinesCount; i++)
|
|
{
|
|
var index = positions[i];
|
|
var x = index % Width;
|
|
var y = index / Width;
|
|
cells[x, y].IsMine = true;
|
|
}
|
|
}
|
|
|
|
private void Shuffle(List<int> values)
|
|
{
|
|
for (var i = values.Count - 1; i > 0; i--)
|
|
{
|
|
var j = random.Next(i + 1);
|
|
(values[i], values[j]) = (values[j], values[i]);
|
|
}
|
|
}
|
|
|
|
private void CalculateNeighborMines()
|
|
{
|
|
for (var x = 0; x < Width; x++)
|
|
{
|
|
for (var y = 0; y < Height; y++)
|
|
{
|
|
if (cells[x, y].IsMine)
|
|
{
|
|
cells[x, y].NeighborMines = 0;
|
|
continue;
|
|
}
|
|
|
|
cells[x, y].NeighborMines = CountNeighborMines(x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
private int CountNeighborMines(int centerX, int centerY)
|
|
{
|
|
var count = 0;
|
|
for (var x = centerX - 1; x <= centerX + 1; x++)
|
|
{
|
|
for (var y = centerY - 1; y <= centerY + 1; y++)
|
|
{
|
|
if (x == centerX && y == centerY)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsInside(x, y) && cells[x, y].IsMine)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private void RevealEmptyArea(int startX, int startY)
|
|
{
|
|
var visited = new bool[Width, Height];
|
|
var queue = new Queue<CellData>();
|
|
queue.Enqueue(cells[startX, startY]);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var cell = queue.Dequeue();
|
|
if (visited[cell.X, cell.Y] || cell.IsMine || cell.IsFlagged)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
visited[cell.X, cell.Y] = true;
|
|
OpenSafeCell(cell);
|
|
|
|
if (cell.NeighborMines != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (var x = cell.X - 1; x <= cell.X + 1; x++)
|
|
{
|
|
for (var y = cell.Y - 1; y <= cell.Y + 1; y++)
|
|
{
|
|
if ((x == cell.X && y == cell.Y) || !IsInside(x, y))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var neighbor = cells[x, y];
|
|
if (!visited[x, y] && !neighbor.IsMine && !neighbor.IsFlagged && !neighbor.IsOpened)
|
|
{
|
|
queue.Enqueue(neighbor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OpenSafeCell(CellData cell)
|
|
{
|
|
if (cell.IsOpened || cell.IsMine)
|
|
{
|
|
return;
|
|
}
|
|
|
|
cell.IsOpened = true;
|
|
OpenedSafeCellsCount++;
|
|
}
|
|
|
|
private bool IsWin()
|
|
{
|
|
return OpenedSafeCellsCount >= SafeCellsCount;
|
|
}
|
|
|
|
private int ToIndex(int x, int y)
|
|
{
|
|
return y * Width + x;
|
|
}
|
|
|
|
private static BoardCellData ToData(CellData cell)
|
|
{
|
|
return new BoardCellData(cell.X, cell.Y, cell.IsMine, cell.IsOpened, cell.IsFlagged, cell.NeighborMines);
|
|
}
|
|
|
|
private sealed class CellData
|
|
{
|
|
public CellData(int x, int y)
|
|
{
|
|
X = x;
|
|
Y = y;
|
|
}
|
|
|
|
public int X { get; }
|
|
public int Y { get; }
|
|
public bool IsMine { get; set; }
|
|
public bool IsOpened { get; set; }
|
|
public bool IsFlagged { get; set; }
|
|
public int NeighborMines { get; set; }
|
|
}
|
|
}
|
|
}
|