[Fix] Rename Scripts Folder

This commit is contained in:
2026-06-07 00:30:10 +07:00
parent 79a928ae52
commit 6c9cdaf67d
140 changed files with 0 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a7c135ae68601e5439b15457e8027a3f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,69 @@
using System;
namespace Minesweeper.Commands
{
public sealed class GameCommandDispatcher : IGameCommandDispatcher
{
private readonly SelectFieldCommandHandler selectFieldHandler;
private readonly StartGameCommandHandler startGameHandler;
private readonly OpenCellCommandHandler openCellHandler;
private readonly ToggleFlagCommandHandler toggleFlagHandler;
private readonly RestartCommandHandler restartHandler;
private readonly PauseCommandHandler pauseHandler;
private readonly ResumeCommandHandler resumeHandler;
private readonly GoToMenuCommandHandler goToMenuHandler;
public GameCommandDispatcher(
SelectFieldCommandHandler selectFieldHandler,
StartGameCommandHandler startGameHandler,
OpenCellCommandHandler openCellHandler,
ToggleFlagCommandHandler toggleFlagHandler,
RestartCommandHandler restartHandler,
PauseCommandHandler pauseHandler,
ResumeCommandHandler resumeHandler,
GoToMenuCommandHandler goToMenuHandler)
{
this.selectFieldHandler = selectFieldHandler;
this.startGameHandler = startGameHandler;
this.openCellHandler = openCellHandler;
this.toggleFlagHandler = toggleFlagHandler;
this.restartHandler = restartHandler;
this.pauseHandler = pauseHandler;
this.resumeHandler = resumeHandler;
this.goToMenuHandler = goToMenuHandler;
}
public void Dispatch<TCommand>(TCommand command) where TCommand : IGameCommand
{
switch (command)
{
case SelectFieldCommand selectFieldCommand:
selectFieldHandler.Handle(selectFieldCommand);
return;
case StartGameCommand startGameCommand:
startGameHandler.Handle(startGameCommand);
return;
case OpenCellCommand openCellCommand:
openCellHandler.Handle(openCellCommand);
return;
case ToggleFlagCommand toggleFlagCommand:
toggleFlagHandler.Handle(toggleFlagCommand);
return;
case RestartCommand restartCommand:
restartHandler.Handle(restartCommand);
return;
case PauseCommand pauseCommand:
pauseHandler.Handle(pauseCommand);
return;
case ResumeCommand resumeCommand:
resumeHandler.Handle(resumeCommand);
return;
case GoToMenuCommand goToMenuCommand:
goToMenuHandler.Handle(goToMenuCommand);
return;
default:
throw new InvalidOperationException($"No handler registered for command {typeof(TCommand).Name}.");
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6383c559964ec3545a7cd911b90586ce
@@ -0,0 +1,246 @@
using Minesweeper.Core;
using Minesweeper.ECS;
namespace Minesweeper.Commands
{
public sealed class SelectFieldCommandHandler : IGameCommandHandler<SelectFieldCommand>
{
private readonly IGameStateService gameStateService;
public SelectFieldCommandHandler(IGameStateService gameStateService)
{
this.gameStateService = gameStateService;
}
public void Handle(SelectFieldCommand command)
{
gameStateService.SetState(GameState.FieldSelection);
}
}
public sealed class StartGameCommandHandler : IGameCommandHandler<StartGameCommand>
{
private readonly IBoardEcsSyncService boardEcsSyncService;
private readonly IBoardService boardService;
private readonly IGamePauseService pauseService;
private readonly IGameStateService gameStateService;
private readonly IGameTimerService timerService;
public StartGameCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGamePauseService pauseService, IGameStateService gameStateService, IGameTimerService timerService)
{
this.boardService = boardService;
this.boardEcsSyncService = boardEcsSyncService;
this.pauseService = pauseService;
this.gameStateService = gameStateService;
this.timerService = timerService;
}
public void Handle(StartGameCommand command)
{
pauseService.Resume();
timerService.Reset();
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;
private readonly IGamePauseService pauseService;
public OpenCellCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService, IGamePauseService pauseService)
{
this.boardService = boardService;
this.boardEcsSyncService = boardEcsSyncService;
this.gameStateService = gameStateService;
this.pauseService = pauseService;
}
public void Handle(OpenCellCommand command)
{
if (pauseService.IsPaused)
{
return;
}
var state = gameStateService.Current;
if (state != GameState.Preparing && state != GameState.Playing)
{
return;
}
if (state == GameState.Preparing)
{
if (boardService.TryGetCell(command.X, command.Y, out var cell) && cell.IsFlagged)
{
return;
}
if (!boardService.GenerateAfterFirstClick(command.X, command.Y))
{
return;
}
}
var result = boardService.OpenCell(command.X, command.Y);
if (result.Invalid || !result.Changed)
{
return;
}
if (result.HitMine)
{
gameStateService.SetState(GameState.Lost);
}
else if (result.Won)
{
gameStateService.SetState(GameState.Won);
}
else if (state == GameState.Preparing)
{
gameStateService.SetState(GameState.Playing);
}
boardEcsSyncService.SyncBoard(boardService);
boardEcsSyncService.SyncGameState(gameStateService.Current, boardService.IsGenerated);
}
}
public sealed class ToggleFlagCommandHandler : IGameCommandHandler<ToggleFlagCommand>
{
private readonly IBoardEcsSyncService boardEcsSyncService;
private readonly IBoardService boardService;
private readonly IGameStateService gameStateService;
private readonly IGamePauseService pauseService;
public ToggleFlagCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService, IGamePauseService pauseService)
{
this.boardService = boardService;
this.boardEcsSyncService = boardEcsSyncService;
this.gameStateService = gameStateService;
this.pauseService = pauseService;
}
public void Handle(ToggleFlagCommand command)
{
if (pauseService.IsPaused)
{
return;
}
var state = gameStateService.Current;
if (state != GameState.Preparing && state != GameState.Playing)
{
return;
}
var result = boardService.ToggleFlag(command.X, command.Y);
if (result.Invalid || !result.Changed)
{
return;
}
boardEcsSyncService.SyncBoard(boardService);
boardEcsSyncService.SyncGameState(gameStateService.Current, boardService.IsGenerated);
}
}
public sealed class RestartCommandHandler : IGameCommandHandler<RestartCommand>
{
private readonly IBoardEcsSyncService boardEcsSyncService;
private readonly IBoardService boardService;
private readonly IGamePauseService pauseService;
private readonly IGameStateService gameStateService;
private readonly IGameTimerService timerService;
public RestartCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGamePauseService pauseService, IGameStateService gameStateService, IGameTimerService timerService)
{
this.boardService = boardService;
this.boardEcsSyncService = boardEcsSyncService;
this.pauseService = pauseService;
this.gameStateService = gameStateService;
this.timerService = timerService;
}
public void Handle(RestartCommand command)
{
var shouldNotifyReset = gameStateService.Current == GameState.Preparing;
pauseService.Resume();
timerService.Reset();
boardService.InitializeEmptyBoard();
gameStateService.SetState(GameState.Preparing);
boardEcsSyncService.SyncBoard(boardService);
boardEcsSyncService.SyncGameState(gameStateService.Current, false);
if (shouldNotifyReset)
{
gameStateService.NotifyCurrentStateChanged();
}
}
}
public sealed class PauseCommandHandler : IGameCommandHandler<PauseCommand>
{
private readonly IGamePauseService pauseService;
private readonly IGameStateService gameStateService;
public PauseCommandHandler(IGamePauseService pauseService, IGameStateService gameStateService)
{
this.pauseService = pauseService;
this.gameStateService = gameStateService;
}
public void Handle(PauseCommand command)
{
if (gameStateService.Current == GameState.Playing)
{
pauseService.Pause();
}
}
}
public sealed class ResumeCommandHandler : IGameCommandHandler<ResumeCommand>
{
private readonly IGamePauseService pauseService;
public ResumeCommandHandler(IGamePauseService pauseService)
{
this.pauseService = pauseService;
}
public void Handle(ResumeCommand command)
{
pauseService.Resume();
}
}
public sealed class GoToMenuCommandHandler : IGameCommandHandler<GoToMenuCommand>
{
private readonly IBoardEcsSyncService boardEcsSyncService;
private readonly IGamePauseService pauseService;
private readonly IGameStateService gameStateService;
private readonly IGameTimerService timerService;
public GoToMenuCommandHandler(IBoardEcsSyncService boardEcsSyncService, IGamePauseService pauseService, IGameStateService gameStateService, IGameTimerService timerService)
{
this.boardEcsSyncService = boardEcsSyncService;
this.pauseService = pauseService;
this.gameStateService = gameStateService;
this.timerService = timerService;
}
public void Handle(GoToMenuCommand command)
{
pauseService.Resume();
timerService.Reset();
gameStateService.SetState(GameState.FieldSelection);
boardEcsSyncService.ClearBoard();
boardEcsSyncService.SyncGameState(gameStateService.Current, false);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 31bfefc0901594043aae51cabba89234
+60
View File
@@ -0,0 +1,60 @@
namespace Minesweeper.Commands
{
public readonly struct SelectFieldCommand : IGameCommand
{
public SelectFieldCommand(int width, int height, int minesCount)
{
Width = width;
Height = height;
MinesCount = minesCount;
}
public int Width { get; }
public int Height { get; }
public int MinesCount { get; }
}
public readonly struct StartGameCommand : IGameCommand
{
}
public readonly struct OpenCellCommand : IGameCommand
{
public OpenCellCommand(int x, int y)
{
X = x;
Y = y;
}
public int X { get; }
public int Y { get; }
}
public readonly struct ToggleFlagCommand : IGameCommand
{
public ToggleFlagCommand(int x, int y)
{
X = x;
Y = y;
}
public int X { get; }
public int Y { get; }
}
public readonly struct RestartCommand : IGameCommand
{
}
public readonly struct PauseCommand : IGameCommand
{
}
public readonly struct ResumeCommand : IGameCommand
{
}
public readonly struct GoToMenuCommand : IGameCommand
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9bd93535958ca574999f8ec6de84baa3
+6
View File
@@ -0,0 +1,6 @@
namespace Minesweeper.Commands
{
public interface IGameCommand
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 50cd27fcb4d423e43b2a56cc23badbfb
@@ -0,0 +1,7 @@
namespace Minesweeper.Commands
{
public interface IGameCommandDispatcher
{
void Dispatch<TCommand>(TCommand command) where TCommand : IGameCommand;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f1c291311029640428d2ea8820fdfeaa
@@ -0,0 +1,7 @@
namespace Minesweeper.Commands
{
public interface IGameCommandHandler<in TCommand> where TCommand : IGameCommand
{
void Handle(TCommand command);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e7b5f5892af9c184ba720b2b3f352b32
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a904ba5489d53724692eb73dd2a68f2e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,15 @@
using UnityEngine;
namespace Minesweeper.Config
{
[CreateAssetMenu(fileName = "MinesweeperGameConfig", menuName = "Minesweeper/Game Config")]
public sealed class MinesweeperGameConfig : ScriptableObject
{
[field: SerializeField, Min(1)] public int MinSizeX { get; private set; } = 2;
[field: SerializeField, Min(1)] public int MaxSizeX { get; private set; } = 50;
[field: SerializeField, Min(1)] public int MinSizeY { get; private set; } = 2;
[field: SerializeField, Min(1)] public int MaxSizeY { get; private set; } = 50;
public bool IsValid => MinSizeX > 0 && MinSizeY > 0 && MaxSizeX >= MinSizeX && MaxSizeY >= MinSizeY && MaxSizeX * MaxSizeY > 1;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b4e8d5c36f36bb443b640a85df3e7077
@@ -0,0 +1,16 @@
using System;
using UnityEngine;
namespace Minesweeper.Config
{
[Serializable]
public sealed class MinesweeperScreenCatalog
{
[field: SerializeField] public GameObject MainMenuPanelPrefab { get; private set; }
[field: SerializeField] public GameObject BoardGridPrefab { get; private set; }
[field: SerializeField] public GameObject PausePanelPrefab { get; private set; }
[field: SerializeField] public GameObject ResultPanelPrefab { get; private set; }
public bool IsValid => MainMenuPanelPrefab != null && BoardGridPrefab != null && PausePanelPrefab != null && ResultPanelPrefab != null;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6d0009ea7e70ee548a09cd95d482ec83
@@ -0,0 +1,76 @@
using UnityEngine;
namespace Minesweeper.Config
{
[CreateAssetMenu(fileName = "MinesweeperUiConfig", menuName = "Minesweeper/UI Config")]
public sealed class MinesweeperUiConfig : ScriptableObject
{
public const float DefaultReferenceCellSize = 100f;
public const float DefaultBoardPaddingRatio = 0.15f;
public const float DefaultGridSpacingRatio = 0.02f;
public const float DefaultMinimumCellSize = 8f;
[field: SerializeField] public Sprite SmileSprite { get; private set; }
[field: SerializeField] public Sprite SurprisedSprite { get; private set; }
[field: SerializeField] public Sprite CoolSprite { get; private set; }
[field: SerializeField] public Sprite DeadSprite { get; private set; }
[field: SerializeField] public GameObject CellButtonPrefab { get; private set; }
[field: SerializeField] public Sprite MineSprite { get; private set; }
[field: SerializeField] public Sprite FlagSprite { get; private set; }
[field: SerializeField] public Color ClosedCellColor { get; private set; } = Color.gray;
[field: SerializeField] public Color OpenedCellColor { get; private set; } = Color.white;
[SerializeField] private Color defaultTextColor = Color.black;
[SerializeField] private Color oneMineTextColor = Color.blue;
[SerializeField] private Color twoMineTextColor = Color.green;
[SerializeField] private Color threeMineTextColor = Color.red;
[SerializeField] private Color fourMineTextColor = new Color(0f, 0f, 0.5f, 1f);
[SerializeField] private Color fiveMineTextColor = new Color(0.5f, 0f, 0f, 1f);
[SerializeField] private Color sixMineTextColor = Color.cyan;
[SerializeField] private Color sevenMineTextColor = Color.black;
[SerializeField] private Color eightMineTextColor = Color.gray;
[field: SerializeField, Min(DefaultMinimumCellSize)] public float ReferenceCellSize { get; private set; } = DefaultReferenceCellSize;
[field: SerializeField, Min(0f)] public float BoardPaddingRatio { get; private set; } = DefaultBoardPaddingRatio;
[field: SerializeField, Min(0f)] public float GridSpacingRatio { get; private set; } = DefaultGridSpacingRatio;
[field: SerializeField, Min(DefaultMinimumCellSize)] public float MinimumCellSize { get; private set; } = DefaultMinimumCellSize;
public Sprite GetSmileSprite(SmileFaceState state)
{
switch (state)
{
case SmileFaceState.Surprised:
return SurprisedSprite != null ? SurprisedSprite : SmileSprite;
case SmileFaceState.Cool:
return CoolSprite != null ? CoolSprite : SmileSprite;
case SmileFaceState.Dead:
return DeadSprite != null ? DeadSprite : SmileSprite;
default:
return SmileSprite;
}
}
public Color GetNumberTextColor(int neighborMines)
{
switch (neighborMines)
{
case 1:
return oneMineTextColor;
case 2:
return twoMineTextColor;
case 3:
return threeMineTextColor;
case 4:
return fourMineTextColor;
case 5:
return fiveMineTextColor;
case 6:
return sixMineTextColor;
case 7:
return sevenMineTextColor;
case 8:
return eightMineTextColor;
default:
return defaultTextColor;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a3b0d5a2a71d45ad9f4ac4f77158c101
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+10
View File
@@ -0,0 +1,10 @@
namespace Minesweeper.Config
{
public enum SmileFaceState
{
Smile = 0,
Surprised = 1,
Cool = 2,
Dead = 3
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f984c51b7db249ac9b9887b2b2bc75ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 391f2b5ec87a6634694fe2c31faee82e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+21
View File
@@ -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
+23
View File
@@ -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
+309
View File
@@ -0,0 +1,309 @@
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; }
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 11dec36e720c3e745b9dc11d588b5794
+33
View File
@@ -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,81 @@
using Minesweeper.Config;
using UnityEngine;
namespace Minesweeper.Core
{
public sealed class GameSettingsService : IGameSettingsService
{
private const int MinimumMinesCount = 1;
private readonly MinesweeperGameConfig config;
private readonly IGameSettingsStorage storage;
private GameSettingsValue current;
public GameSettingsService(MinesweeperGameConfig config, IGameSettingsStorage storage)
{
this.config = config;
this.storage = storage;
current = LoadInitialSettings();
}
public int SizeX => current.SizeX;
public int SizeY => current.SizeY;
public int MinesCount => current.MinesCount;
public GameSettingsValue Current => current;
public GameSettingsValue Clamp(GameSettingsValue value)
{
var sizeX = Mathf.Clamp(value.SizeX, config.MinSizeX, config.MaxSizeX);
var sizeY = Mathf.Clamp(value.SizeY, config.MinSizeY, config.MaxSizeY);
EnsureAtLeastTwoCells(ref sizeX, ref sizeY);
var mines = Mathf.Clamp(value.MinesCount, MinimumMinesCount, GetMaxMines(sizeX, sizeY));
return new GameSettingsValue(sizeX, sizeY, mines);
}
public bool ApplyAndSaveIfChanged(GameSettingsValue value)
{
var clamped = Clamp(value);
if (current.Equals(clamped))
{
return false;
}
current = clamped;
storage.Save(current);
return true;
}
public int GetMaxMines(int sizeX, int sizeY)
{
return Mathf.Max(MinimumMinesCount, sizeX * sizeY - 1);
}
private GameSettingsValue LoadInitialSettings()
{
if (storage.TryLoad(out var saved))
{
return Clamp(saved);
}
return Clamp(new GameSettingsValue(config.MinSizeX, config.MinSizeY, MinimumMinesCount));
}
private void EnsureAtLeastTwoCells(ref int sizeX, ref int sizeY)
{
if (sizeX * sizeY > 1)
{
return;
}
if (sizeX < config.MaxSizeX)
{
sizeX++;
}
else if (sizeY < config.MaxSizeY)
{
sizeY++;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 38b7be3d055b5c34bbba9b36c0e11fc6
+21
View File
@@ -0,0 +1,21 @@
namespace Minesweeper.Core
{
public readonly struct GameSettingsValue
{
public GameSettingsValue(int sizeX, int sizeY, int minesCount)
{
SizeX = sizeX;
SizeY = sizeY;
MinesCount = minesCount;
}
public int SizeX { get; }
public int SizeY { get; }
public int MinesCount { get; }
public bool Equals(GameSettingsValue other)
{
return SizeX == other.SizeX && SizeY == other.SizeY && MinesCount == other.MinesCount;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5a48645a1753ceb4ba0401ec44d70070
+11
View File
@@ -0,0 +1,11 @@
namespace Minesweeper.Core
{
public enum GameState
{
FieldSelection,
Preparing,
Playing,
Lost,
Won
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c06ebf54d6bacdf4888fabbf29bea1cd
+27
View File
@@ -0,0 +1,27 @@
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);
}
public void NotifyCurrentStateChanged()
{
StateChanged?.Invoke(Current);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cf69805439993c14887ea7bb9b15bd02
+48
View File
@@ -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
+22
View File
@@ -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
+14
View File
@@ -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,14 @@
namespace Minesweeper.Core
{
public interface IGameSettingsService
{
int SizeX { get; }
int SizeY { get; }
int MinesCount { get; }
GameSettingsValue Current { get; }
GameSettingsValue Clamp(GameSettingsValue value);
bool ApplyAndSaveIfChanged(GameSettingsValue value);
int GetMaxMines(int sizeX, int sizeY);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3dcff3a940f664b4b96722f28e46636d
@@ -0,0 +1,8 @@
namespace Minesweeper.Core
{
public interface IGameSettingsStorage
{
bool TryLoad(out GameSettingsValue value);
void Save(GameSettingsValue value);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 54d04c5398018bc418e1a2528c559d0a
+14
View File
@@ -0,0 +1,14 @@
using System;
namespace Minesweeper.Core
{
public interface IGameStateService
{
event Action<GameState> StateChanged;
GameState Current { get; }
void SetState(GameState state);
void NotifyCurrentStateChanged();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7aea04a8c0e8d3a4e8d991a4348430db
+13
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ebd50c14109e96541aa49542a07986aa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+120
View File
@@ -0,0 +1,120 @@
using Minesweeper.Core;
using Minesweeper.ECS.Components;
using Unity.Collections;
using Unity.Entities;
namespace Minesweeper.ECS
{
public sealed class BoardEcsSyncService : IBoardEcsSyncService
{
public void ClearBoard()
{
if (!TryGetEntityManager(out var entityManager))
{
return;
}
ClearCells(entityManager);
}
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
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2ebb3bd7d4baf544fadefb7718c935d2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
using Unity.Entities;
namespace Minesweeper.ECS.Components
{
public struct BoardConfigComponent : IComponentData
{
public int Width;
public int Height;
public int MinesCount;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b0024f7b9432b3740bdb6ae9ab132529
@@ -0,0 +1,14 @@
using Unity.Entities;
namespace Minesweeper.ECS.Components
{
public struct CellComponent : IComponentData
{
public int X;
public int Y;
public byte IsMine;
public byte IsOpened;
public byte IsFlagged;
public int NeighborMines;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 50ce9bb3f8ca1c142a28adb88899afec
@@ -0,0 +1,11 @@
using Minesweeper.Core;
using Unity.Entities;
namespace Minesweeper.ECS.Components
{
public struct GameStateComponent : IComponentData
{
public GameState State;
public byte HasFirstClick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a89195585a555b54e909b4f8797f20ed
@@ -0,0 +1,11 @@
using Minesweeper.Core;
namespace Minesweeper.ECS
{
public interface IBoardEcsSyncService
{
void ClearBoard();
void SyncBoard(IBoardService boardService);
void SyncGameState(GameState state, bool hasFirstClick);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 779db0eb469cc2449bce502b6e46231d
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 210d08f5d5948674fa0df51ed6a785b0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cbb80c6f3d55e8d418e4f3369b2e1623
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
using System;
using Minesweeper.Presentation.Presenters;
using VContainer.Unity;
namespace Minesweeper.Infrastructure
{
public sealed class MinesweeperEntryPoint : IStartable, IDisposable
{
private readonly MainMenuPresenter mainMenuPresenter;
private readonly TopPanelPresenter topPanelPresenter;
private readonly GamePresenter gamePresenter;
public MinesweeperEntryPoint(MainMenuPresenter mainMenuPresenter, TopPanelPresenter topPanelPresenter, GamePresenter gamePresenter)
{
this.mainMenuPresenter = mainMenuPresenter;
this.topPanelPresenter = topPanelPresenter;
this.gamePresenter = gamePresenter;
}
public void Start()
{
topPanelPresenter.Initialize();
mainMenuPresenter.Initialize();
gamePresenter.Initialize();
}
public void Dispose()
{
gamePresenter.Dispose();
mainMenuPresenter.Dispose();
topPanelPresenter.Dispose();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5eae713b4d801be4996a70d4b630eeee
@@ -0,0 +1,158 @@
using Minesweeper.Commands;
using Minesweeper.Config;
using Minesweeper.Core;
using Minesweeper.ECS;
using Minesweeper.Presentation.Adapters;
using Minesweeper.Presentation.Factories;
using Minesweeper.Presentation.Presenters;
using Minesweeper.Presentation.ReadModels;
using Minesweeper.Presentation.Views;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace Minesweeper.Infrastructure
{
public sealed class MinesweeperLifetimeScope : LifetimeScope
{
[SerializeField] private MinesweeperGameConfig gameConfig;
[SerializeField] private MinesweeperUiConfig uiConfig;
[SerializeField] private MinesweeperScreenCatalog screenCatalog = new MinesweeperScreenCatalog();
[SerializeField] private Transform contentRoot;
[SerializeField] private TopPanelView topPanelView;
[SerializeField] private MainMenuView mainMenuView;
[SerializeField] private BoardView boardView;
[SerializeField] private PauseView pauseView;
[SerializeField] private ResultView resultView;
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterInstance(GetConfig());
var resolvedUiConfig = GetUiConfig();
builder.RegisterInstance(resolvedUiConfig);
builder.Register<PlayerPrefsGameSettingsStorage>(Lifetime.Singleton).As<IGameSettingsStorage>();
builder.Register<GameSettingsService>(Lifetime.Singleton).As<IGameSettingsService>();
builder.Register<GameStateService>(Lifetime.Singleton).As<IGameStateService>();
builder.Register<BoardService>(Lifetime.Singleton).As<IBoardService>();
builder.Register<GamePauseService>(Lifetime.Singleton).As<IGamePauseService>();
builder.Register<GameTimerService>(Lifetime.Singleton).As<IGameTimerService>().As<ITickable>();
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>();
var screenRefs = SpawnScreens();
if (screenRefs.MainMenuView != null)
{
mainMenuView = screenRefs.MainMenuView;
}
if (screenRefs.BoardView != null)
{
boardView = screenRefs.BoardView;
}
if (screenRefs.PauseView != null)
{
pauseView = screenRefs.PauseView;
}
if (screenRefs.ResultView != null)
{
resultView = screenRefs.ResultView;
}
if (topPanelView != null)
{
topPanelView.BindConfig(resolvedUiConfig);
builder.RegisterComponent(topPanelView).As<ITopPanelView>();
}
else
{
builder.Register<NullTopPanelView>(Lifetime.Singleton).As<ITopPanelView>();
}
if (mainMenuView != null)
{
builder.RegisterComponent(mainMenuView).As<IMainMenuView>();
}
else
{
builder.Register<NullMainMenuView>(Lifetime.Singleton).As<IMainMenuView>();
}
if (boardView != null)
{
boardView.BindConfig(resolvedUiConfig);
builder.RegisterComponent(boardView).As<IBoardView>();
}
else
{
builder.Register<NullBoardView>(Lifetime.Singleton).As<IBoardView>();
}
if (pauseView != null)
{
builder.RegisterComponent(pauseView).As<IPauseView>();
}
else
{
builder.Register<NullPauseView>(Lifetime.Singleton).As<IPauseView>();
}
if (resultView != null)
{
builder.RegisterComponent(resultView).As<IResultView>();
}
else
{
builder.Register<NullResultView>(Lifetime.Singleton).As<IResultView>();
}
builder.Register<SelectFieldCommandHandler>(Lifetime.Singleton);
builder.Register<StartGameCommandHandler>(Lifetime.Singleton);
builder.Register<OpenCellCommandHandler>(Lifetime.Singleton);
builder.Register<ToggleFlagCommandHandler>(Lifetime.Singleton);
builder.Register<RestartCommandHandler>(Lifetime.Singleton);
builder.Register<PauseCommandHandler>(Lifetime.Singleton);
builder.Register<ResumeCommandHandler>(Lifetime.Singleton);
builder.Register<GoToMenuCommandHandler>(Lifetime.Singleton);
builder.Register<GameCommandDispatcher>(Lifetime.Singleton).As<IGameCommandDispatcher>();
builder.Register<MainMenuPresenter>(Lifetime.Singleton);
builder.Register<TopPanelPresenter>(Lifetime.Singleton);
builder.Register<GamePresenter>(Lifetime.Singleton);
builder.RegisterEntryPoint<MinesweeperEntryPoint>();
}
private MinesweeperGameConfig GetConfig()
{
if (gameConfig != null)
{
return gameConfig;
}
return ScriptableObject.CreateInstance<MinesweeperGameConfig>();
}
private MinesweeperUiConfig GetUiConfig()
{
if (uiConfig != null)
{
return uiConfig;
}
return ScriptableObject.CreateInstance<MinesweeperUiConfig>();
}
private MinesweeperScreenRefs SpawnScreens()
{
if (contentRoot == null || screenCatalog == null || !screenCatalog.IsValid)
{
return default;
}
return new MinesweeperScreenBootstrapper().Spawn(contentRoot, screenCatalog);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d4f9b0c2ad803d84382fbf03ba3096fa
@@ -0,0 +1,32 @@
using Minesweeper.Core;
using UnityEngine;
namespace Minesweeper.Infrastructure
{
public sealed class PlayerPrefsGameSettingsStorage : IGameSettingsStorage
{
private const string SizeXKey = "Minesweeper.Settings.SizeX";
private const string SizeYKey = "Minesweeper.Settings.SizeY";
private const string MinesCountKey = "Minesweeper.Settings.MinesCount";
public bool TryLoad(out GameSettingsValue value)
{
if (!PlayerPrefs.HasKey(SizeXKey) || !PlayerPrefs.HasKey(SizeYKey) || !PlayerPrefs.HasKey(MinesCountKey))
{
value = default;
return false;
}
value = new GameSettingsValue(PlayerPrefs.GetInt(SizeXKey), PlayerPrefs.GetInt(SizeYKey), PlayerPrefs.GetInt(MinesCountKey));
return true;
}
public void Save(GameSettingsValue value)
{
PlayerPrefs.SetInt(SizeXKey, value.SizeX);
PlayerPrefs.SetInt(SizeYKey, value.SizeY);
PlayerPrefs.SetInt(MinesCountKey, value.MinesCount);
PlayerPrefs.Save();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a001fc2e924d95643a075190b6cadda1
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b1435ba5f7e9b514cb863ee06385cb77
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 648cba2aa826df04aa6de2236d532ff7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,12 @@
using Minesweeper.Core;
namespace Minesweeper.Presentation.Adapters
{
public sealed class GameStateViewAdapter : IGameStateViewAdapter
{
public string GetDisplayName(GameState state)
{
return state.ToString();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a10e635e4da16d24db457ead4d139896
@@ -0,0 +1,9 @@
using Minesweeper.Core;
namespace Minesweeper.Presentation.Adapters
{
public interface IGameStateViewAdapter
{
string GetDisplayName(GameState state);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 058bab5bc0f87f64dbbf668e5e390216
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ec6192ca6506ac64c8aa089466c86db5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using Minesweeper.Config;
using Minesweeper.Core;
using Minesweeper.Presentation.Views;
using UnityEngine;
namespace Minesweeper.Presentation.Factories
{
public sealed class CellViewFactory : ICellViewFactory
{
private readonly MinesweeperUiConfig uiConfig;
public CellViewFactory(MinesweeperUiConfig uiConfig)
{
this.uiConfig = uiConfig;
}
public CellView CreateCell(BoardCellData cell, Transform parent)
{
var prefab = uiConfig.CellButtonPrefab;
if (prefab == null)
{
Debug.LogError("CellViewFactory failed: CellButtonPrefab is not assigned.");
return null;
}
var go = Object.Instantiate(prefab, parent);
go.name = BuildCellName(cell.X, cell.Y, cell.DisplayValue);
var view = go.GetComponent<CellView>();
if (view == null)
{
Debug.LogError($"CellViewFactory failed: '{prefab.name}' is missing CellView.");
Object.Destroy(go);
return null;
}
view.Initialize(cell.X, cell.Y);
return view;
}
public string BuildCellName(int x, int y, int value, bool isMine)
{
return $"bt_{x}_{y}_{(isMine ? "M" : value.ToString())}";
}
public string BuildCellName(int x, int y, string displayValue)
{
return $"bt_{x}_{y}_{displayValue}";
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 681a16a970ddf3546bb4f99d93a184ca
@@ -0,0 +1,13 @@
using Minesweeper.Core;
using Minesweeper.Presentation.Views;
using UnityEngine;
namespace Minesweeper.Presentation.Factories
{
public interface ICellViewFactory
{
CellView CreateCell(BoardCellData cell, Transform parent);
string BuildCellName(int x, int y, int value, bool isMine);
string BuildCellName(int x, int y, string displayValue);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 34c31a0f01c730440a2bcdac0f775dd8
@@ -0,0 +1,102 @@
using Minesweeper.Config;
using Minesweeper.Presentation.Views;
using UnityEngine;
namespace Minesweeper.Presentation.Factories
{
public sealed class MinesweeperScreenBootstrapper
{
private const string MainMenuPanelName = "MainMenuPanel";
private const string BoardGridName = "BoardGrid";
private const string PausePanelName = "PausePanel";
private const string ResultPanelName = "ResultPanel";
public MinesweeperScreenRefs Spawn(Transform contentRoot, MinesweeperScreenCatalog catalog)
{
if (contentRoot == null)
{
Debug.LogError("Minesweeper screen bootstrap failed: Content root is not assigned.");
return default;
}
if (catalog == null || !catalog.IsValid)
{
Debug.LogError("Minesweeper screen bootstrap failed: screen catalog prefab references are incomplete.");
return default;
}
ClearContent(contentRoot);
var mainMenu = SpawnScreen(catalog.MainMenuPanelPrefab, contentRoot, MainMenuPanelName, 0);
var board = SpawnScreen(catalog.BoardGridPrefab, contentRoot, BoardGridName, 1);
var pause = SpawnScreen(catalog.PausePanelPrefab, contentRoot, PausePanelName, 2);
var result = SpawnScreen(catalog.ResultPanelPrefab, contentRoot, ResultPanelName, 3);
var mainMenuView = RequireComponent<MainMenuView>(mainMenu.transform, MainMenuPanelName);
if (mainMenuView != null)
{
mainMenuView.BindRoot(mainMenu);
}
var boardView = RequireComponent<BoardView>(board.transform, BoardGridName);
var pauseView = RequireComponent<PauseView>(pause.transform, PausePanelName);
var resultView = RequireComponent<ResultView>(result.transform, ResultPanelName);
var refs = new MinesweeperScreenRefs(
mainMenuView,
boardView,
pauseView,
resultView);
mainMenu.SetActive(false);
board.SetActive(false);
pause.SetActive(false);
result.SetActive(false);
return refs;
}
private static GameObject SpawnScreen(GameObject prefab, Transform parent, string expectedName, int siblingIndex)
{
var instance = Object.Instantiate(prefab, parent, false);
instance.name = expectedName;
instance.transform.SetSiblingIndex(siblingIndex);
Stretch(instance.GetComponent<RectTransform>());
return instance;
}
private static void ClearContent(Transform contentRoot)
{
for (var i = contentRoot.childCount - 1; i >= 0; i--)
{
var child = contentRoot.GetChild(i);
child.SetParent(null, false);
Object.Destroy(child.gameObject);
}
}
private static T RequireComponent<T>(Transform root, string ownerName) where T : Component
{
var component = root.GetComponent<T>();
if (component == null)
{
Debug.LogError($"Minesweeper screen bootstrap failed: '{ownerName}' is missing {typeof(T).Name}.");
}
return component;
}
private static void Stretch(RectTransform rectTransform)
{
if (rectTransform == null)
{
return;
}
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 38a228c86699cdb499351cf807842dfb
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3895ae14930e10841a7562a478c871ec
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,193 @@
using Minesweeper.Commands;
using Minesweeper.Core;
using Minesweeper.Presentation.Factories;
using Minesweeper.Presentation.ReadModels;
using Minesweeper.Presentation.Views;
namespace Minesweeper.Presentation.Presenters
{
public sealed class GamePresenter : IPresenter
{
private readonly IGameCommandDispatcher commandDispatcher;
private readonly ICellViewFactory cellViewFactory;
private readonly IGamePauseService pauseService;
private readonly IGameReadModel readModel;
private readonly IGameStateService gameStateService;
private readonly TopPanelPresenter topPanelPresenter;
private readonly IBoardView boardView;
private readonly IPauseView pauseView;
private readonly IResultView resultView;
private bool boardBuilt;
public GamePresenter(IGameCommandDispatcher commandDispatcher, ICellViewFactory cellViewFactory, IGamePauseService pauseService, IGameReadModel readModel, IGameStateService gameStateService, TopPanelPresenter topPanelPresenter, IBoardView boardView, IPauseView pauseView, IResultView resultView)
{
this.commandDispatcher = commandDispatcher;
this.cellViewFactory = cellViewFactory;
this.pauseService = pauseService;
this.readModel = readModel;
this.gameStateService = gameStateService;
this.topPanelPresenter = topPanelPresenter;
this.boardView = boardView;
this.pauseView = pauseView;
this.resultView = resultView;
}
public void Initialize()
{
boardView.CellPressStarted += OnCellPressStarted;
boardView.CellPressEnded += OnCellPressEnded;
boardView.PauseRequested += OnPauseRequested;
boardView.CellOpenRequested += OnCellOpenRequested;
boardView.CellFlagRequested += OnCellFlagRequested;
pauseView.RestartRequested += OnRestartRequested;
pauseView.ResumeRequested += OnResumeRequested;
pauseView.GoToMenuRequested += OnGoToMenuRequested;
resultView.RestartRequested += OnRestartRequested;
resultView.GoToMenuRequested += OnGoToMenuRequested;
gameStateService.StateChanged += OnStateChanged;
pauseService.PauseChanged += OnPauseChanged;
OnStateChanged(gameStateService.Current);
OnPauseChanged(pauseService.IsPaused);
}
public void Dispose()
{
boardView.CellPressStarted -= OnCellPressStarted;
boardView.CellPressEnded -= OnCellPressEnded;
boardView.PauseRequested -= OnPauseRequested;
boardView.CellOpenRequested -= OnCellOpenRequested;
boardView.CellFlagRequested -= OnCellFlagRequested;
pauseView.RestartRequested -= OnRestartRequested;
pauseView.ResumeRequested -= OnResumeRequested;
pauseView.GoToMenuRequested -= OnGoToMenuRequested;
resultView.RestartRequested -= OnRestartRequested;
resultView.GoToMenuRequested -= OnGoToMenuRequested;
gameStateService.StateChanged -= OnStateChanged;
pauseService.PauseChanged -= OnPauseChanged;
}
private void OnCellOpenRequested(int x, int y)
{
topPanelPresenter.SetCellPressActive(false);
commandDispatcher.Dispatch(new OpenCellCommand(x, y));
RefreshBoard();
topPanelPresenter.RefreshCounters();
UpdateBoardInput();
}
private void OnCellFlagRequested(int x, int y)
{
commandDispatcher.Dispatch(new ToggleFlagCommand(x, y));
RefreshBoard();
topPanelPresenter.RefreshCounters();
UpdateBoardInput();
}
private void OnCellPressStarted()
{
topPanelPresenter.SetCellPressActive(true);
}
private void OnCellPressEnded()
{
topPanelPresenter.SetCellPressActive(false);
}
private void OnStateChanged(GameState state)
{
if (state == GameState.FieldSelection)
{
boardBuilt = false;
boardView.Hide();
pauseView.Hide();
resultView.Hide();
return;
}
boardView.Show();
if (state == GameState.Won || state == GameState.Lost)
{
pauseView.Hide();
resultView.Show(state);
}
else
{
resultView.Hide();
}
if (!boardBuilt || state == GameState.Preparing)
{
RebuildBoard();
}
else
{
RefreshBoard();
}
UpdateBoardInput();
}
private void OnPauseChanged(bool isPaused)
{
if (isPaused && gameStateService.Current == GameState.Playing)
{
pauseView.Show();
}
else
{
pauseView.Hide();
}
UpdateBoardInput();
}
private void RebuildBoard()
{
var cells = readModel.GetCells();
boardView.Rebuild(cells, readModel.Width, readModel.Height, cellViewFactory, IsFinalState());
boardBuilt = true;
topPanelPresenter.RefreshCounters();
UpdateBoardInput();
}
private void RefreshBoard()
{
boardView.Refresh(readModel.GetCells(), IsFinalState());
}
private bool IsFinalState()
{
var state = gameStateService.Current;
return state == GameState.Lost || state == GameState.Won;
}
private void UpdateBoardInput()
{
var state = gameStateService.Current;
boardView.SetInputEnabled(!pauseService.IsPaused && (state == GameState.Preparing || state == GameState.Playing));
}
private void OnRestartRequested()
{
commandDispatcher.Dispatch(new RestartCommand());
RebuildBoard();
topPanelPresenter.RefreshCounters();
}
private void OnGoToMenuRequested()
{
commandDispatcher.Dispatch(new GoToMenuCommand());
}
private void OnResumeRequested()
{
commandDispatcher.Dispatch(new ResumeCommand());
}
private void OnPauseRequested()
{
commandDispatcher.Dispatch(new PauseCommand());
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9f85646fc4851054e9efffa3ab1f6853
@@ -0,0 +1,9 @@
using System;
namespace Minesweeper.Presentation.Presenters
{
public interface IPresenter : IDisposable
{
void Initialize();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e0fcee9aa500b62429eb4ffa2249de9c
@@ -0,0 +1,81 @@
using Minesweeper.Commands;
using Minesweeper.Config;
using Minesweeper.Core;
using Minesweeper.Presentation.Views;
namespace Minesweeper.Presentation.Presenters
{
public sealed class MainMenuPresenter : IPresenter
{
private readonly IGameCommandDispatcher commandDispatcher;
private readonly MinesweeperGameConfig config;
private readonly IGameSettingsService settingsService;
private readonly IGameStateService gameStateService;
private readonly IMainMenuView view;
public MainMenuPresenter(IGameCommandDispatcher commandDispatcher, MinesweeperGameConfig config, IGameSettingsService settingsService, IGameStateService gameStateService, IMainMenuView view = null)
{
this.commandDispatcher = commandDispatcher;
this.config = config;
this.settingsService = settingsService;
this.gameStateService = gameStateService;
this.view = view;
}
public void Initialize()
{
if (view != null)
{
view.StartClicked += OnStartClicked;
view.SizeChanged += OnSizeChanged;
gameStateService.StateChanged += OnStateChanged;
RefreshMenuValues(settingsService.Current);
OnStateChanged(gameStateService.Current);
}
}
public void Dispose()
{
if (view != null)
{
view.StartClicked -= OnStartClicked;
view.SizeChanged -= OnSizeChanged;
gameStateService.StateChanged -= OnStateChanged;
}
}
private void OnStartClicked()
{
settingsService.ApplyAndSaveIfChanged(view.SelectedSettings);
commandDispatcher.Dispatch(new StartGameCommand());
}
private void OnSizeChanged()
{
var selected = settingsService.Clamp(view.SelectedSettings);
var maxMines = settingsService.GetMaxMines(selected.SizeX, selected.SizeY);
view.ConfigureMines(1, maxMines, selected.MinesCount);
}
private void OnStateChanged(GameState state)
{
if (state == GameState.FieldSelection)
{
RefreshMenuValues(settingsService.Current);
view.Show();
}
else
{
view.Hide();
}
}
private void RefreshMenuValues(GameSettingsValue settings)
{
var clamped = settingsService.Clamp(settings);
view.ConfigureSizeX(config.MinSizeX, config.MaxSizeX, clamped.SizeX);
view.ConfigureSizeY(config.MinSizeY, config.MaxSizeY, clamped.SizeY);
view.ConfigureMines(1, settingsService.GetMaxMines(clamped.SizeX, clamped.SizeY), clamped.MinesCount);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 835a3b63609e9864fa6154be8b8283ad
@@ -0,0 +1,130 @@
using Minesweeper.Commands;
using Minesweeper.Config;
using Minesweeper.Core;
using Minesweeper.Presentation.ReadModels;
using Minesweeper.Presentation.Views;
namespace Minesweeper.Presentation.Presenters
{
public sealed class TopPanelPresenter : IPresenter
{
private readonly IGameCommandDispatcher commandDispatcher;
private readonly IGamePauseService pauseService;
private readonly IGameReadModel readModel;
private readonly IGameStateService gameStateService;
private readonly IGameTimerService timerService;
private readonly ITopPanelView view;
public TopPanelPresenter(IGameCommandDispatcher commandDispatcher, IGamePauseService pauseService, IGameReadModel readModel, IGameStateService gameStateService, IGameTimerService timerService, ITopPanelView view)
{
this.commandDispatcher = commandDispatcher;
this.pauseService = pauseService;
this.readModel = readModel;
this.gameStateService = gameStateService;
this.timerService = timerService;
this.view = view;
}
public void Initialize()
{
view.SmileClicked += OnSmileClicked;
gameStateService.StateChanged += OnStateChanged;
pauseService.PauseChanged += OnPauseChanged;
timerService.TimeChanged += OnTimeChanged;
RefreshAll();
}
public void Dispose()
{
view.SmileClicked -= OnSmileClicked;
gameStateService.StateChanged -= OnStateChanged;
pauseService.PauseChanged -= OnPauseChanged;
timerService.TimeChanged -= OnTimeChanged;
}
public void RefreshCounters()
{
view.SetRemainingMines(readModel.RemainingMinesCount);
}
public void SetCellPressActive(bool active)
{
var state = gameStateService.Current;
if (active && !pauseService.IsPaused && (state == GameState.Preparing || state == GameState.Playing))
{
view.SetSmile(SmileFaceState.Surprised);
return;
}
view.SetSmile(GetSmileState(state));
}
private void RefreshAll()
{
view.SetActive(true);
view.SetRemainingMines(readModel.RemainingMinesCount);
view.SetTimer(timerService.ElapsedSeconds);
view.SetSmile(GetSmileState(gameStateService.Current));
}
private void OnSmileClicked()
{
var state = gameStateService.Current;
if (state == GameState.FieldSelection)
{
commandDispatcher.Dispatch(new StartGameCommand());
}
else if (state == GameState.Playing && !pauseService.IsPaused)
{
commandDispatcher.Dispatch(new PauseCommand());
}
else if (state == GameState.Playing && pauseService.IsPaused)
{
commandDispatcher.Dispatch(new ResumeCommand());
}
else
{
commandDispatcher.Dispatch(new RestartCommand());
}
RefreshAll();
}
private void OnStateChanged(GameState state)
{
if (state == GameState.FieldSelection || state == GameState.Preparing)
{
view.SetTimer(0f);
}
view.SetSmile(GetSmileState(state));
view.SetRemainingMines(readModel.RemainingMinesCount);
}
private void OnPauseChanged(bool isPaused)
{
view.SetSmile(GetSmileState(gameStateService.Current));
}
private void OnTimeChanged(float seconds)
{
view.SetTimer(seconds);
}
private SmileFaceState GetSmileState(GameState state)
{
if (state == GameState.Won)
{
return SmileFaceState.Cool;
}
if (state == GameState.Lost)
{
return SmileFaceState.Dead;
}
return SmileFaceState.Smile;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3ac57cd2e63497d9d0d7e23659a4d45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 590e96d08af41e346b49a7ce43641afb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using Minesweeper.Core;
namespace Minesweeper.Presentation.ReadModels
{
public sealed class GameReadModel : IGameReadModel
{
private readonly IBoardService boardService;
private readonly IGameSettingsService settingsService;
private readonly IGameStateService gameStateService;
public GameReadModel(IBoardService boardService, IGameSettingsService settingsService, IGameStateService gameStateService)
{
this.boardService = boardService;
this.settingsService = settingsService;
this.gameStateService = gameStateService;
}
public GameState State => gameStateService.Current;
public int Width => boardService.Width > 0 ? boardService.Width : settingsService.SizeX;
public int Height => boardService.Height > 0 ? boardService.Height : settingsService.SizeY;
public int MinesCount => boardService.MinesCount > 0 ? boardService.MinesCount : settingsService.MinesCount;
public int FlaggedCellsCount => CountFlaggedCells();
public int RemainingMinesCount => MinesCount - FlaggedCellsCount;
public bool TryGetCell(int x, int y, out BoardCellData cell)
{
return boardService.TryGetCell(x, y, out cell);
}
public IReadOnlyList<BoardCellData> GetCells()
{
return boardService.GetCells();
}
private int CountFlaggedCells()
{
var cells = boardService.GetCells();
var count = 0;
for (var i = 0; i < cells.Count; i++)
{
if (cells[i].IsFlagged)
{
count++;
}
}
return count;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1b2a816449e034a4f9635960881ec852
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using Minesweeper.Core;
namespace Minesweeper.Presentation.ReadModels
{
public interface IGameReadModel
{
GameState State { get; }
int Width { get; }
int Height { get; }
int MinesCount { get; }
int FlaggedCellsCount { get; }
int RemainingMinesCount { get; }
bool TryGetCell(int x, int y, out BoardCellData cell);
IReadOnlyList<BoardCellData> GetCells();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e5c921f20dd6f1b40a1f73746265a63d

Some files were not shown because too many files have changed in this diff Show More