[Fix] UI Logic
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public readonly struct BoardActionResult
|
||||
{
|
||||
public BoardActionResult(bool changed, bool hitMine, bool won, bool invalid)
|
||||
{
|
||||
Changed = changed;
|
||||
HitMine = hitMine;
|
||||
Won = won;
|
||||
Invalid = invalid;
|
||||
}
|
||||
|
||||
public bool Changed { get; }
|
||||
public bool HitMine { get; }
|
||||
public bool Won { get; }
|
||||
public bool Invalid { get; }
|
||||
|
||||
public static BoardActionResult NoChange => new BoardActionResult(false, false, false, false);
|
||||
public static BoardActionResult InvalidAction => new BoardActionResult(false, false, false, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7457e882fbca4e644959ccff78510436
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public readonly struct BoardCellData
|
||||
{
|
||||
public BoardCellData(int x, int y, bool isMine, bool isOpened, bool isFlagged, int neighborMines)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
IsMine = isMine;
|
||||
IsOpened = isOpened;
|
||||
IsFlagged = isFlagged;
|
||||
NeighborMines = neighborMines;
|
||||
}
|
||||
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public bool IsMine { get; }
|
||||
public bool IsOpened { get; }
|
||||
public bool IsFlagged { get; }
|
||||
public int NeighborMines { get; }
|
||||
public string DisplayValue => IsMine ? "M" : NeighborMines.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 832de90f32c3d7d4d8526f82eb203866
|
||||
@@ -0,0 +1,330 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11dec36e720c3e745b9dc11d588b5794
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public sealed class GamePauseService : IGamePauseService
|
||||
{
|
||||
public event Action<bool> PauseChanged;
|
||||
|
||||
public bool IsPaused { get; private set; }
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
if (IsPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsPaused = true;
|
||||
PauseChanged?.Invoke(IsPaused);
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
if (!IsPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsPaused = false;
|
||||
PauseChanged?.Invoke(IsPaused);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40441c28481279147959eafabd8a032a
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public enum GameState
|
||||
{
|
||||
FieldSelection,
|
||||
Preparing,
|
||||
Playing,
|
||||
Lost,
|
||||
Won
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c06ebf54d6bacdf4888fabbf29bea1cd
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public sealed class GameStateService : IGameStateService
|
||||
{
|
||||
public event Action<GameState> StateChanged;
|
||||
|
||||
public GameState Current { get; private set; } = GameState.FieldSelection;
|
||||
|
||||
public void SetState(GameState state)
|
||||
{
|
||||
if (Current == state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Current = state;
|
||||
StateChanged?.Invoke(Current);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf69805439993c14887ea7bb9b15bd02
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public sealed class GameTimerService : IGameTimerService, ITickable
|
||||
{
|
||||
private readonly IGamePauseService pauseService;
|
||||
private readonly IGameStateService gameStateService;
|
||||
private int lastReportedSeconds = -1;
|
||||
|
||||
public GameTimerService(IGameStateService gameStateService, IGamePauseService pauseService)
|
||||
{
|
||||
this.gameStateService = gameStateService;
|
||||
this.pauseService = pauseService;
|
||||
}
|
||||
|
||||
public event Action<float> TimeChanged;
|
||||
|
||||
public float ElapsedSeconds { get; private set; }
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (gameStateService.Current != GameState.Playing || pauseService.IsPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ElapsedSeconds += Time.deltaTime;
|
||||
var seconds = Mathf.FloorToInt(ElapsedSeconds);
|
||||
if (seconds == lastReportedSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lastReportedSeconds = seconds;
|
||||
TimeChanged?.Invoke(ElapsedSeconds);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
ElapsedSeconds = 0f;
|
||||
lastReportedSeconds = -1;
|
||||
TimeChanged?.Invoke(ElapsedSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed8262be24a32a04abfd5bc5ec8544bb
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public interface IBoardService
|
||||
{
|
||||
int Width { get; }
|
||||
int Height { get; }
|
||||
int MinesCount { get; }
|
||||
bool IsGenerated { get; }
|
||||
int OpenedSafeCellsCount { get; }
|
||||
int SafeCellsCount { get; }
|
||||
|
||||
void InitializeEmptyBoard();
|
||||
bool GenerateAfterFirstClick(int safeX, int safeY);
|
||||
BoardActionResult OpenCell(int x, int y);
|
||||
BoardActionResult ToggleFlag(int x, int y);
|
||||
bool IsInside(int x, int y);
|
||||
bool TryGetCell(int x, int y, out BoardCellData cell);
|
||||
IReadOnlyList<BoardCellData> GetCells();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1339a8a92f1f56d4fa786ce011294737
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public interface IGamePauseService
|
||||
{
|
||||
event Action<bool> PauseChanged;
|
||||
|
||||
bool IsPaused { get; }
|
||||
|
||||
void Pause();
|
||||
void Resume();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82dfb9fe1e7004f4f88df366f8e76b2d
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public interface IGameStateService
|
||||
{
|
||||
event Action<GameState> StateChanged;
|
||||
|
||||
GameState Current { get; }
|
||||
|
||||
void SetState(GameState state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7aea04a8c0e8d3a4e8d991a4348430db
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public interface IGameTimerService
|
||||
{
|
||||
event Action<float> TimeChanged;
|
||||
|
||||
float ElapsedSeconds { get; }
|
||||
|
||||
void Reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61242d395cb1d974daffd9e0815ec34c
|
||||
Reference in New Issue
Block a user