using System; using System.Collections.Generic; using Minesweeper.Config; namespace Minesweeper.Core { public sealed class BoardService : IBoardService { private const int DefaultWidth = 9; private const int DefaultHeight = 9; private const int DefaultMinesCount = 10; private readonly MinesweeperGameConfig config; private readonly Random random = new Random(); private CellData[,] cells; public BoardService(MinesweeperGameConfig config) { this.config = config; } 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() { ResolveConfig(out var width, out var height, out var minesCount); Width = width; Height = height; MinesCount = minesCount; 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 GetCells() { EnsureInitialized(); var result = new List(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 ResolveConfig(out int width, out int height, out int minesCount) { width = config.Width; height = config.Height; minesCount = config.MinesCount; if (width <= 0 || height <= 0 || minesCount <= 0 || minesCount >= width * height) { width = DefaultWidth; height = DefaultHeight; minesCount = DefaultMinesCount; } } private void EnsureInitialized() { if (cells == null) { InitializeEmptyBoard(); } } private void PlaceMines(int safeX, int safeY) { var positions = new List(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 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(); 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; } } } }