[Add] Field and mine generation
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.ECS;
|
||||
|
||||
namespace Minesweeper.Commands
|
||||
{
|
||||
@@ -19,23 +20,54 @@ namespace Minesweeper.Commands
|
||||
|
||||
public sealed class StartGameCommandHandler : IGameCommandHandler<StartGameCommand>
|
||||
{
|
||||
private readonly IBoardEcsSyncService boardEcsSyncService;
|
||||
private readonly IBoardService boardService;
|
||||
private readonly IGameStateService gameStateService;
|
||||
|
||||
public StartGameCommandHandler(IGameStateService gameStateService)
|
||||
public StartGameCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService)
|
||||
{
|
||||
this.boardService = boardService;
|
||||
this.boardEcsSyncService = boardEcsSyncService;
|
||||
this.gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
public void Handle(StartGameCommand command)
|
||||
{
|
||||
boardService.InitializeEmptyBoard();
|
||||
gameStateService.SetState(GameState.Preparing);
|
||||
boardEcsSyncService.SyncBoard(boardService);
|
||||
boardEcsSyncService.SyncGameState(gameStateService.Current, false);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class OpenCellCommandHandler : IGameCommandHandler<OpenCellCommand>
|
||||
{
|
||||
private readonly IBoardEcsSyncService boardEcsSyncService;
|
||||
private readonly IBoardService boardService;
|
||||
private readonly IGameStateService gameStateService;
|
||||
|
||||
public OpenCellCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService)
|
||||
{
|
||||
this.boardService = boardService;
|
||||
this.boardEcsSyncService = boardEcsSyncService;
|
||||
this.gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
public void Handle(OpenCellCommand command)
|
||||
{
|
||||
if (gameStateService.Current != GameState.Preparing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!boardService.GenerateAfterFirstClick(command.X, command.Y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
gameStateService.SetState(GameState.Playing);
|
||||
boardEcsSyncService.SyncBoard(boardService);
|
||||
boardEcsSyncService.SyncGameState(gameStateService.Current, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,16 +80,23 @@ namespace Minesweeper.Commands
|
||||
|
||||
public sealed class RestartCommandHandler : IGameCommandHandler<RestartCommand>
|
||||
{
|
||||
private readonly IBoardEcsSyncService boardEcsSyncService;
|
||||
private readonly IBoardService boardService;
|
||||
private readonly IGameStateService gameStateService;
|
||||
|
||||
public RestartCommandHandler(IGameStateService gameStateService)
|
||||
public RestartCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService)
|
||||
{
|
||||
this.boardService = boardService;
|
||||
this.boardEcsSyncService = boardEcsSyncService;
|
||||
this.gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
public void Handle(RestartCommand command)
|
||||
{
|
||||
boardService.InitializeEmptyBoard();
|
||||
gameStateService.SetState(GameState.Preparing);
|
||||
boardEcsSyncService.SyncBoard(boardService);
|
||||
boardEcsSyncService.SyncGameState(gameStateService.Current, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,16 +116,19 @@ namespace Minesweeper.Commands
|
||||
|
||||
public sealed class GoToMenuCommandHandler : IGameCommandHandler<GoToMenuCommand>
|
||||
{
|
||||
private readonly IBoardEcsSyncService boardEcsSyncService;
|
||||
private readonly IGameStateService gameStateService;
|
||||
|
||||
public GoToMenuCommandHandler(IGameStateService gameStateService)
|
||||
public GoToMenuCommandHandler(IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService)
|
||||
{
|
||||
this.boardEcsSyncService = boardEcsSyncService;
|
||||
this.gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
public void Handle(GoToMenuCommand command)
|
||||
{
|
||||
gameStateService.SetState(GameState.FieldSelection);
|
||||
boardEcsSyncService.SyncGameState(gameStateService.Current, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,219 @@
|
||||
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 void InitializeEmptyBoard()
|
||||
{
|
||||
ResolveConfig(out var width, out var height, out var minesCount);
|
||||
|
||||
Width = width;
|
||||
Height = height;
|
||||
MinesCount = minesCount;
|
||||
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();
|
||||
|
||||
cells[safeX, safeY].IsOpened = true;
|
||||
IsGenerated = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
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 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,18 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public interface IBoardService
|
||||
{
|
||||
int Width { get; }
|
||||
int Height { get; }
|
||||
int MinesCount { get; }
|
||||
bool IsGenerated { get; }
|
||||
|
||||
void InitializeEmptyBoard();
|
||||
bool GenerateAfterFirstClick(int safeX, int safeY);
|
||||
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,110 @@
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.ECS.Components;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
|
||||
namespace Minesweeper.ECS
|
||||
{
|
||||
public sealed class BoardEcsSyncService : IBoardEcsSyncService
|
||||
{
|
||||
public void SyncBoard(IBoardService boardService)
|
||||
{
|
||||
if (!TryGetEntityManager(out var entityManager))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearCells(entityManager);
|
||||
|
||||
var boardEntity = GetOrCreateSingleton<BoardConfigComponent>(entityManager);
|
||||
entityManager.SetComponentData(boardEntity, new BoardConfigComponent
|
||||
{
|
||||
Width = boardService.Width,
|
||||
Height = boardService.Height,
|
||||
MinesCount = boardService.MinesCount
|
||||
});
|
||||
|
||||
var archetype = entityManager.CreateArchetype(typeof(CellComponent));
|
||||
var cells = boardService.GetCells();
|
||||
for (var i = 0; i < cells.Count; i++)
|
||||
{
|
||||
var cell = cells[i];
|
||||
var entity = entityManager.CreateEntity(archetype);
|
||||
entityManager.SetComponentData(entity, new CellComponent
|
||||
{
|
||||
X = cell.X,
|
||||
Y = cell.Y,
|
||||
IsMine = ToByte(cell.IsMine),
|
||||
IsOpened = ToByte(cell.IsOpened),
|
||||
IsFlagged = ToByte(cell.IsFlagged),
|
||||
NeighborMines = cell.NeighborMines
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void SyncGameState(GameState state, bool hasFirstClick)
|
||||
{
|
||||
if (!TryGetEntityManager(out var entityManager))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stateEntity = GetOrCreateSingleton<GameStateComponent>(entityManager);
|
||||
entityManager.SetComponentData(stateEntity, new GameStateComponent
|
||||
{
|
||||
State = state,
|
||||
HasFirstClick = ToByte(hasFirstClick)
|
||||
});
|
||||
}
|
||||
|
||||
private static bool TryGetEntityManager(out EntityManager entityManager)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated)
|
||||
{
|
||||
entityManager = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
entityManager = world.EntityManager;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ClearCells(EntityManager entityManager)
|
||||
{
|
||||
var query = entityManager.CreateEntityQuery(typeof(CellComponent));
|
||||
entityManager.DestroyEntity(query);
|
||||
query.Dispose();
|
||||
}
|
||||
|
||||
private static Entity GetOrCreateSingleton<T>(EntityManager entityManager) where T : unmanaged, IComponentData
|
||||
{
|
||||
var query = entityManager.CreateEntityQuery(typeof(T));
|
||||
Entity entity;
|
||||
|
||||
if (query.IsEmptyIgnoreFilter)
|
||||
{
|
||||
entity = entityManager.CreateEntity(typeof(T));
|
||||
}
|
||||
else
|
||||
{
|
||||
var entities = query.ToEntityArray(Allocator.Temp);
|
||||
entity = entities[0];
|
||||
for (var i = 1; i < entities.Length; i++)
|
||||
{
|
||||
entityManager.DestroyEntity(entities[i]);
|
||||
}
|
||||
|
||||
entities.Dispose();
|
||||
}
|
||||
|
||||
query.Dispose();
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static byte ToByte(bool value)
|
||||
{
|
||||
return value ? (byte)1 : (byte)0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ad76f74587f648429ed4e14a39e4d13
|
||||
@@ -0,0 +1,10 @@
|
||||
using Minesweeper.Core;
|
||||
|
||||
namespace Minesweeper.ECS
|
||||
{
|
||||
public interface IBoardEcsSyncService
|
||||
{
|
||||
void SyncBoard(IBoardService boardService);
|
||||
void SyncGameState(GameState state, bool hasFirstClick);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 779db0eb469cc2449bce502b6e46231d
|
||||
@@ -1,6 +1,7 @@
|
||||
using Minesweeper.Commands;
|
||||
using Minesweeper.Config;
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.ECS;
|
||||
using Minesweeper.Presentation.Adapters;
|
||||
using Minesweeper.Presentation.Factories;
|
||||
using Minesweeper.Presentation.Presenters;
|
||||
@@ -20,6 +21,8 @@ namespace Minesweeper.Infrastructure
|
||||
{
|
||||
builder.RegisterInstance(GetConfig());
|
||||
builder.Register<GameStateService>(Lifetime.Singleton).As<IGameStateService>();
|
||||
builder.Register<BoardService>(Lifetime.Singleton).As<IBoardService>();
|
||||
builder.Register<BoardEcsSyncService>(Lifetime.Singleton).As<IBoardEcsSyncService>();
|
||||
builder.Register<GameReadModel>(Lifetime.Singleton).As<IGameReadModel>();
|
||||
builder.Register<GameStateViewAdapter>(Lifetime.Singleton).As<IGameStateViewAdapter>();
|
||||
builder.Register<CellViewFactory>(Lifetime.Singleton).As<ICellViewFactory>();
|
||||
|
||||
@@ -6,5 +6,10 @@ namespace Minesweeper.Presentation.Factories
|
||||
{
|
||||
return $"bt_{x}_{y}_{(isMine ? "M" : value.ToString())}";
|
||||
}
|
||||
|
||||
public string BuildCellName(int x, int y, string displayValue)
|
||||
{
|
||||
return $"bt_{x}_{y}_{displayValue}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ namespace Minesweeper.Presentation.Factories
|
||||
public interface ICellViewFactory
|
||||
{
|
||||
string BuildCellName(int x, int y, int value, bool isMine);
|
||||
string BuildCellName(int x, int y, string displayValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Minesweeper.Config;
|
||||
using Minesweeper.Core;
|
||||
|
||||
@@ -6,17 +7,29 @@ namespace Minesweeper.Presentation.ReadModels
|
||||
public sealed class GameReadModel : IGameReadModel
|
||||
{
|
||||
private readonly MinesweeperGameConfig config;
|
||||
private readonly IBoardService boardService;
|
||||
private readonly IGameStateService gameStateService;
|
||||
|
||||
public GameReadModel(MinesweeperGameConfig config, IGameStateService gameStateService)
|
||||
public GameReadModel(MinesweeperGameConfig config, IBoardService boardService, IGameStateService gameStateService)
|
||||
{
|
||||
this.config = config;
|
||||
this.boardService = boardService;
|
||||
this.gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
public GameState State => gameStateService.Current;
|
||||
public int Width => config.Width;
|
||||
public int Height => config.Height;
|
||||
public int MinesCount => config.MinesCount;
|
||||
public int Width => boardService.Width > 0 ? boardService.Width : config.Width;
|
||||
public int Height => boardService.Height > 0 ? boardService.Height : config.Height;
|
||||
public int MinesCount => boardService.MinesCount > 0 ? boardService.MinesCount : config.MinesCount;
|
||||
|
||||
public bool TryGetCell(int x, int y, out BoardCellData cell)
|
||||
{
|
||||
return boardService.TryGetCell(x, y, out cell);
|
||||
}
|
||||
|
||||
public IReadOnlyList<BoardCellData> GetCells()
|
||||
{
|
||||
return boardService.GetCells();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Minesweeper.Core;
|
||||
|
||||
namespace Minesweeper.Presentation.ReadModels
|
||||
@@ -8,5 +9,8 @@ namespace Minesweeper.Presentation.ReadModels
|
||||
int Width { get; }
|
||||
int Height { get; }
|
||||
int MinesCount { get; }
|
||||
|
||||
bool TryGetCell(int x, int y, out BoardCellData cell);
|
||||
IReadOnlyList<BoardCellData> GetCells();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user