Files
FreewayGamesTest/Assets/Minesweeper/Runtime/Core/BoardService.cs
T

331 lines
8.9 KiB
C#

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