[Fix] UI Logic
This commit is contained in:
@@ -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,239 @@
|
||||
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)
|
||||
{
|
||||
pauseService.Resume();
|
||||
timerService.Reset();
|
||||
boardService.InitializeEmptyBoard();
|
||||
gameStateService.SetState(GameState.Preparing);
|
||||
boardEcsSyncService.SyncBoard(boardService);
|
||||
boardEcsSyncService.SyncGameState(gameStateService.Current, false);
|
||||
}
|
||||
}
|
||||
|
||||
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.SyncGameState(gameStateService.Current, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31bfefc0901594043aae51cabba89234
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a904ba5489d53724692eb73dd2a68f2e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minesweeper.Config
|
||||
{
|
||||
[CreateAssetMenu(fileName = "MinesweeperGameConfig", menuName = "Minesweeper/Game Config")]
|
||||
public sealed class MinesweeperGameConfig : ScriptableObject
|
||||
{
|
||||
[SerializeField, Min(1)] private int width = 9;
|
||||
[SerializeField, Min(1)] private int height = 9;
|
||||
[SerializeField, Min(1)] private int minesCount = 10;
|
||||
[SerializeField] private KeyCode restartKey = KeyCode.R;
|
||||
|
||||
public int Width => width;
|
||||
public int Height => height;
|
||||
public int MinesCount => minesCount;
|
||||
public KeyCode RestartKey => restartKey;
|
||||
|
||||
public bool IsValid => width > 0 && height > 0 && minesCount > 0 && minesCount < width * height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4e8d5c36f36bb443b640a85df3e7077
|
||||
@@ -0,0 +1,86 @@
|
||||
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;
|
||||
|
||||
[SerializeField] private Sprite smileSprite;
|
||||
[SerializeField] private Sprite surprisedSprite;
|
||||
[SerializeField] private Sprite coolSprite;
|
||||
[SerializeField] private Sprite deadSprite;
|
||||
[SerializeField] private GameObject cellButtonPrefab;
|
||||
[SerializeField] private Sprite mineSprite;
|
||||
[SerializeField] private Sprite flagSprite;
|
||||
[SerializeField] private Sprite closedCellSprite;
|
||||
[SerializeField] private Sprite openedCellSprite;
|
||||
[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;
|
||||
[SerializeField, Min(DefaultMinimumCellSize)] private float referenceCellSize = DefaultReferenceCellSize;
|
||||
[SerializeField, Min(0f)] private float boardPaddingRatio = DefaultBoardPaddingRatio;
|
||||
[SerializeField, Min(0f)] private float gridSpacingRatio = DefaultGridSpacingRatio;
|
||||
[SerializeField, Min(DefaultMinimumCellSize)] private float minimumCellSize = DefaultMinimumCellSize;
|
||||
|
||||
public GameObject CellButtonPrefab => cellButtonPrefab;
|
||||
public Sprite MineSprite => mineSprite;
|
||||
public Sprite FlagSprite => flagSprite;
|
||||
public Sprite ClosedCellSprite => closedCellSprite;
|
||||
public Sprite OpenedCellSprite => openedCellSprite;
|
||||
public float ReferenceCellSize => referenceCellSize;
|
||||
public float BoardPaddingRatio => boardPaddingRatio;
|
||||
public float GridSpacingRatio => gridSpacingRatio;
|
||||
public float MinimumCellSize => minimumCellSize;
|
||||
|
||||
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:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 391f2b5ec87a6634694fe2c31faee82e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ebd50c14109e96541aa49542a07986aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,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,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
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 210d08f5d5948674fa0df51ed6a785b0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,104 @@
|
||||
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 TopPanelView topPanelView;
|
||||
[SerializeField] private MainMenuView mainMenuView;
|
||||
[SerializeField] private GameView gameView;
|
||||
|
||||
protected override void Configure(IContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterInstance(GetConfig());
|
||||
var resolvedUiConfig = GetUiConfig();
|
||||
builder.RegisterInstance(resolvedUiConfig);
|
||||
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>();
|
||||
|
||||
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 (gameView != null)
|
||||
{
|
||||
gameView.BindConfig(resolvedUiConfig);
|
||||
builder.RegisterComponent(gameView).As<IGameView>();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Register<NullGameView>(Lifetime.Singleton).As<IGameView>();
|
||||
}
|
||||
|
||||
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<RestartKeyInputService>(Lifetime.Singleton).As<ITickable>();
|
||||
|
||||
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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4f9b0c2ad803d84382fbf03ba3096fa
|
||||
@@ -0,0 +1,50 @@
|
||||
using Minesweeper.Commands;
|
||||
using Minesweeper.Config;
|
||||
using Minesweeper.Core;
|
||||
using UnityEngine.InputSystem;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Minesweeper.Infrastructure
|
||||
{
|
||||
public sealed class RestartKeyInputService : ITickable
|
||||
{
|
||||
private readonly IGameCommandDispatcher commandDispatcher;
|
||||
private readonly MinesweeperGameConfig config;
|
||||
private readonly IGameStateService gameStateService;
|
||||
|
||||
public RestartKeyInputService(IGameCommandDispatcher commandDispatcher, MinesweeperGameConfig config, IGameStateService gameStateService)
|
||||
{
|
||||
this.commandDispatcher = commandDispatcher;
|
||||
this.config = config;
|
||||
this.gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
var keyboard = Keyboard.current;
|
||||
if (keyboard == null || !IsRestartPressed(keyboard))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameStateService.Current == GameState.FieldSelection)
|
||||
{
|
||||
commandDispatcher.Dispatch(new StartGameCommand());
|
||||
}
|
||||
else
|
||||
{
|
||||
commandDispatcher.Dispatch(new RestartCommand());
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsRestartPressed(Keyboard keyboard)
|
||||
{
|
||||
if (config.RestartKey == UnityEngine.KeyCode.R)
|
||||
{
|
||||
return keyboard.rKey.wasPressedThisFrame;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c87c15d092dd420b85a09cc786496948
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,91 @@
|
||||
using Minesweeper.Config;
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.Presentation.Views;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minesweeper.Presentation.Factories
|
||||
{
|
||||
public sealed class CellViewFactory : ICellViewFactory
|
||||
{
|
||||
private const string ContentImagePath = "Content/Image";
|
||||
private const string ContentLabelPath = "Content/Text (TMP)";
|
||||
|
||||
private readonly MinesweeperUiConfig uiConfig;
|
||||
|
||||
public CellViewFactory(MinesweeperUiConfig uiConfig)
|
||||
{
|
||||
this.uiConfig = uiConfig;
|
||||
}
|
||||
|
||||
public CellView CreateCell(BoardCellData cell, Transform parent)
|
||||
{
|
||||
var prefab = uiConfig.CellButtonPrefab;
|
||||
var go = prefab != null ? Object.Instantiate(prefab, parent) : CreateFallbackCell(parent);
|
||||
go.name = BuildCellName(cell.X, cell.Y, cell.DisplayValue);
|
||||
|
||||
var view = go.GetComponent<CellView>();
|
||||
if (view == null)
|
||||
{
|
||||
view = go.AddComponent<CellView>();
|
||||
}
|
||||
|
||||
var button = go.GetComponent<Button>();
|
||||
var backgroundImage = go.GetComponent<Image>();
|
||||
var contentImage = FindComponent<Image>(go.transform, ContentImagePath);
|
||||
var label = FindComponent<TMP_Text>(go.transform, ContentLabelPath);
|
||||
view.Bind(button, backgroundImage, contentImage, label);
|
||||
view.AutoBind();
|
||||
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}";
|
||||
}
|
||||
|
||||
private static T FindComponent<T>(Transform root, string path) where T : Component
|
||||
{
|
||||
var child = root.Find(path);
|
||||
return child != null ? child.GetComponent<T>() : null;
|
||||
}
|
||||
|
||||
private static GameObject CreateFallbackCell(Transform parent)
|
||||
{
|
||||
var go = new GameObject("Cell", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button));
|
||||
go.transform.SetParent(parent, false);
|
||||
|
||||
var content = new GameObject("Content", typeof(RectTransform));
|
||||
content.transform.SetParent(go.transform, false);
|
||||
Stretch(content.GetComponent<RectTransform>());
|
||||
|
||||
var image = new GameObject("Image", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
image.transform.SetParent(content.transform, false);
|
||||
Stretch(image.GetComponent<RectTransform>());
|
||||
|
||||
var text = new GameObject("Text (TMP)", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
|
||||
text.transform.SetParent(content.transform, false);
|
||||
Stretch(text.GetComponent<RectTransform>());
|
||||
|
||||
var label = text.GetComponent<TextMeshProUGUI>();
|
||||
label.alignment = TextAlignmentOptions.Center;
|
||||
label.enableAutoSizing = true;
|
||||
return go;
|
||||
}
|
||||
|
||||
private static void Stretch(RectTransform rectTransform)
|
||||
{
|
||||
rectTransform.anchorMin = Vector2.zero;
|
||||
rectTransform.anchorMax = Vector2.one;
|
||||
rectTransform.offsetMin = Vector2.zero;
|
||||
rectTransform.offsetMax = Vector2.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3895ae14930e10841a7562a478c871ec
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,196 @@
|
||||
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 IGameTimerService timerService;
|
||||
private readonly TopPanelPresenter topPanelPresenter;
|
||||
private readonly IGameView view;
|
||||
private bool boardBuilt;
|
||||
|
||||
public GamePresenter(IGameCommandDispatcher commandDispatcher, ICellViewFactory cellViewFactory, IGamePauseService pauseService, IGameReadModel readModel, IGameStateService gameStateService, IGameTimerService timerService, TopPanelPresenter topPanelPresenter, IGameView view = null)
|
||||
{
|
||||
this.commandDispatcher = commandDispatcher;
|
||||
this.cellViewFactory = cellViewFactory;
|
||||
this.pauseService = pauseService;
|
||||
this.readModel = readModel;
|
||||
this.gameStateService = gameStateService;
|
||||
this.timerService = timerService;
|
||||
this.topPanelPresenter = topPanelPresenter;
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (view != null)
|
||||
{
|
||||
view.RestartRequested += OnRestartRequested;
|
||||
view.GoToMenuRequested += OnGoToMenuRequested;
|
||||
view.PauseRequested += OnPauseRequested;
|
||||
view.ResumeRequested += OnResumeRequested;
|
||||
view.CellPressStarted += OnCellPressStarted;
|
||||
view.CellPressEnded += OnCellPressEnded;
|
||||
view.CellOpenRequested += OnCellOpenRequested;
|
||||
view.CellFlagRequested += OnCellFlagRequested;
|
||||
gameStateService.StateChanged += OnStateChanged;
|
||||
pauseService.PauseChanged += OnPauseChanged;
|
||||
timerService.TimeChanged += OnTimeChanged;
|
||||
OnStateChanged(gameStateService.Current);
|
||||
OnPauseChanged(pauseService.IsPaused);
|
||||
OnTimeChanged(timerService.ElapsedSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (view != null)
|
||||
{
|
||||
view.RestartRequested -= OnRestartRequested;
|
||||
view.GoToMenuRequested -= OnGoToMenuRequested;
|
||||
view.PauseRequested -= OnPauseRequested;
|
||||
view.ResumeRequested -= OnResumeRequested;
|
||||
view.CellPressStarted -= OnCellPressStarted;
|
||||
view.CellPressEnded -= OnCellPressEnded;
|
||||
view.CellOpenRequested -= OnCellOpenRequested;
|
||||
view.CellFlagRequested -= OnCellFlagRequested;
|
||||
gameStateService.StateChanged -= OnStateChanged;
|
||||
pauseService.PauseChanged -= OnPauseChanged;
|
||||
timerService.TimeChanged -= OnTimeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRestartRequested()
|
||||
{
|
||||
commandDispatcher.Dispatch(new RestartCommand());
|
||||
RebuildBoard();
|
||||
topPanelPresenter.RefreshCounters();
|
||||
}
|
||||
|
||||
private void OnGoToMenuRequested()
|
||||
{
|
||||
commandDispatcher.Dispatch(new GoToMenuCommand());
|
||||
}
|
||||
|
||||
private void OnPauseRequested()
|
||||
{
|
||||
commandDispatcher.Dispatch(new PauseCommand());
|
||||
}
|
||||
|
||||
private void OnResumeRequested()
|
||||
{
|
||||
commandDispatcher.Dispatch(new ResumeCommand());
|
||||
}
|
||||
|
||||
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;
|
||||
view.HideGame();
|
||||
view.HidePause();
|
||||
view.HideResult();
|
||||
return;
|
||||
}
|
||||
|
||||
view.ShowGame();
|
||||
|
||||
if (state == GameState.Won || state == GameState.Lost)
|
||||
{
|
||||
view.HidePause();
|
||||
view.ShowResult(state);
|
||||
}
|
||||
else
|
||||
{
|
||||
view.HideResult();
|
||||
}
|
||||
|
||||
if (!boardBuilt || state == GameState.Preparing)
|
||||
{
|
||||
RebuildBoard();
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshBoard();
|
||||
}
|
||||
|
||||
UpdateBoardInput();
|
||||
}
|
||||
|
||||
private void OnPauseChanged(bool isPaused)
|
||||
{
|
||||
if (isPaused)
|
||||
{
|
||||
view.ShowPause();
|
||||
}
|
||||
else
|
||||
{
|
||||
view.HidePause();
|
||||
}
|
||||
|
||||
UpdateBoardInput();
|
||||
}
|
||||
|
||||
private void OnTimeChanged(float seconds)
|
||||
{
|
||||
view.SetTimer(seconds);
|
||||
}
|
||||
|
||||
private void RebuildBoard()
|
||||
{
|
||||
var cells = readModel.GetCells();
|
||||
view.SetMineCount(readModel.MinesCount);
|
||||
view.RebuildBoard(cells, readModel.Width, readModel.Height, cellViewFactory);
|
||||
boardBuilt = true;
|
||||
topPanelPresenter.RefreshCounters();
|
||||
UpdateBoardInput();
|
||||
}
|
||||
|
||||
private void RefreshBoard()
|
||||
{
|
||||
view.RefreshBoard(readModel.GetCells());
|
||||
}
|
||||
|
||||
private void UpdateBoardInput()
|
||||
{
|
||||
var state = gameStateService.Current;
|
||||
view.SetBoardInputEnabled(!pauseService.IsPaused && (state == GameState.Preparing || state == GameState.Playing));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,56 @@
|
||||
using Minesweeper.Commands;
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.Presentation.Views;
|
||||
|
||||
namespace Minesweeper.Presentation.Presenters
|
||||
{
|
||||
public sealed class MainMenuPresenter : IPresenter
|
||||
{
|
||||
private readonly IGameCommandDispatcher commandDispatcher;
|
||||
private readonly IGameStateService gameStateService;
|
||||
private readonly IMainMenuView view;
|
||||
|
||||
public MainMenuPresenter(IGameCommandDispatcher commandDispatcher, IGameStateService gameStateService, IMainMenuView view = null)
|
||||
{
|
||||
this.commandDispatcher = commandDispatcher;
|
||||
this.gameStateService = gameStateService;
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (view != null)
|
||||
{
|
||||
view.StartClicked += OnStartClicked;
|
||||
gameStateService.StateChanged += OnStateChanged;
|
||||
OnStateChanged(gameStateService.Current);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (view != null)
|
||||
{
|
||||
view.StartClicked -= OnStartClicked;
|
||||
gameStateService.StateChanged -= OnStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartClicked()
|
||||
{
|
||||
commandDispatcher.Dispatch(new StartGameCommand());
|
||||
}
|
||||
|
||||
private void OnStateChanged(GameState state)
|
||||
{
|
||||
if (state == GameState.FieldSelection)
|
||||
{
|
||||
view.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
view.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,52 @@
|
||||
using System.Collections.Generic;
|
||||
using Minesweeper.Config;
|
||||
using Minesweeper.Core;
|
||||
|
||||
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, IBoardService boardService, IGameStateService gameStateService)
|
||||
{
|
||||
this.config = config;
|
||||
this.boardService = boardService;
|
||||
this.gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
public GameState State => gameStateService.Current;
|
||||
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 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
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e2357d56da983b478c1cebe1e3ac363
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.Config;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
public sealed class CellView : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
|
||||
{
|
||||
private const string ContentImagePath = "Content/Image";
|
||||
private const string ContentLabelPath = "Content/Text (TMP)";
|
||||
|
||||
[SerializeField] private Button button;
|
||||
[SerializeField] private Image backgroundImage;
|
||||
[SerializeField] private Image contentImage;
|
||||
[SerializeField] private TMP_Text label;
|
||||
|
||||
private int x;
|
||||
private int y;
|
||||
private bool inputEnabled = true;
|
||||
private bool isOpened;
|
||||
|
||||
public event Action<int, int> OpenRequested;
|
||||
public event Action<int, int> FlagRequested;
|
||||
public event Action PressStarted;
|
||||
public event Action PressEnded;
|
||||
|
||||
public void Bind(Button button, Image backgroundImage, Image contentImage, TMP_Text label)
|
||||
{
|
||||
this.button = button;
|
||||
this.backgroundImage = backgroundImage;
|
||||
this.contentImage = contentImage;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public void AutoBind()
|
||||
{
|
||||
if (button == null)
|
||||
{
|
||||
button = GetComponent<Button>();
|
||||
}
|
||||
|
||||
if (backgroundImage == null)
|
||||
{
|
||||
backgroundImage = GetComponent<Image>();
|
||||
}
|
||||
|
||||
if (contentImage == null)
|
||||
{
|
||||
var contentImageTransform = transform.Find(ContentImagePath);
|
||||
if (contentImageTransform != null)
|
||||
{
|
||||
contentImage = contentImageTransform.GetComponent<Image>();
|
||||
}
|
||||
}
|
||||
|
||||
if (label == null)
|
||||
{
|
||||
var labelTransform = transform.Find(ContentLabelPath);
|
||||
if (labelTransform != null)
|
||||
{
|
||||
label = labelTransform.GetComponent<TMP_Text>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(int x, int y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public void SetInputEnabled(bool enabled)
|
||||
{
|
||||
inputEnabled = enabled;
|
||||
if (button != null)
|
||||
{
|
||||
button.interactable = enabled && !isOpened;
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(BoardCellData cell, MinesweeperUiConfig config, float pixelsPerUnitMultiplier)
|
||||
{
|
||||
gameObject.name = $"bt_{cell.X}_{cell.Y}_{cell.DisplayValue}";
|
||||
isOpened = cell.IsOpened;
|
||||
|
||||
if (backgroundImage != null)
|
||||
{
|
||||
backgroundImage.pixelsPerUnitMultiplier = pixelsPerUnitMultiplier;
|
||||
backgroundImage.sprite = cell.IsOpened ? config.OpenedCellSprite : config.ClosedCellSprite;
|
||||
backgroundImage.color = Color.white;
|
||||
}
|
||||
|
||||
if (contentImage != null)
|
||||
{
|
||||
contentImage.pixelsPerUnitMultiplier = pixelsPerUnitMultiplier;
|
||||
}
|
||||
|
||||
RenderContent(cell, config);
|
||||
|
||||
if (button != null)
|
||||
{
|
||||
button.interactable = inputEnabled && !cell.IsOpened;
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderContent(BoardCellData cell, MinesweeperUiConfig config)
|
||||
{
|
||||
var showFlag = cell.IsFlagged && !cell.IsOpened;
|
||||
var showMine = cell.IsOpened && cell.IsMine;
|
||||
var showNumber = cell.IsOpened && !cell.IsMine && cell.NeighborMines > 0;
|
||||
|
||||
if (contentImage != null)
|
||||
{
|
||||
contentImage.gameObject.SetActive(showFlag || showMine);
|
||||
if (showFlag)
|
||||
{
|
||||
contentImage.sprite = config.FlagSprite;
|
||||
}
|
||||
else if (showMine)
|
||||
{
|
||||
contentImage.sprite = config.MineSprite;
|
||||
}
|
||||
|
||||
contentImage.color = Color.white;
|
||||
}
|
||||
|
||||
if (label != null)
|
||||
{
|
||||
label.gameObject.SetActive(showNumber);
|
||||
label.text = showNumber ? cell.NeighborMines.ToString() : string.Empty;
|
||||
label.color = config.GetNumberTextColor(cell.NeighborMines);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (!inputEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventData.button == PointerEventData.InputButton.Left)
|
||||
{
|
||||
OpenRequested?.Invoke(x, y);
|
||||
}
|
||||
else if (eventData.button == PointerEventData.InputButton.Right)
|
||||
{
|
||||
FlagRequested?.Invoke(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
if (inputEnabled && eventData.button == PointerEventData.InputButton.Left)
|
||||
{
|
||||
PressStarted?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerUp(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button == PointerEventData.InputButton.Left)
|
||||
{
|
||||
PressEnded?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2904d462d22809c499afe1842f6e6239
|
||||
@@ -0,0 +1,360 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Minesweeper.Config;
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.Presentation.Factories;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
public sealed class GameView : MonoBehaviour, IGameView
|
||||
{
|
||||
[SerializeField] private GameObject gameRoot;
|
||||
[SerializeField] private GameObject pauseRoot;
|
||||
[SerializeField] private GameObject resultRoot;
|
||||
[SerializeField] private RectTransform boardPanel;
|
||||
[SerializeField] private GridLayoutGroup gridLayoutGroup;
|
||||
[SerializeField] private Button pauseButton;
|
||||
[SerializeField] private Button restartButton;
|
||||
[SerializeField] private Button resumeButton;
|
||||
[SerializeField] private Button mainMenuButton;
|
||||
[SerializeField] private Button resultRestartButton;
|
||||
[SerializeField] private Button resultMainMenuButton;
|
||||
[SerializeField] private TMP_Text resultText;
|
||||
[SerializeField] private TMP_Text timerText;
|
||||
[SerializeField] private TMP_Text mineText;
|
||||
[SerializeField] private MinesweeperUiConfig uiConfig;
|
||||
|
||||
private readonly Dictionary<int, CellView> cellsByCoordinate = new Dictionary<int, CellView>();
|
||||
private bool boardInputEnabled = true;
|
||||
private float currentPixelsPerUnitMultiplier = 1f;
|
||||
|
||||
public event Action RestartRequested;
|
||||
public event Action GoToMenuRequested;
|
||||
public event Action PauseRequested;
|
||||
public event Action ResumeRequested;
|
||||
public event Action CellPressStarted;
|
||||
public event Action CellPressEnded;
|
||||
public event Action<int, int> CellOpenRequested;
|
||||
public event Action<int, int> CellFlagRequested;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (gameRoot == null)
|
||||
{
|
||||
gameRoot = gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
AddButtonListeners();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
RemoveButtonListeners();
|
||||
}
|
||||
|
||||
public void ShowGame()
|
||||
{
|
||||
gameRoot.SetActive(true);
|
||||
SetBoardRootActive(true);
|
||||
}
|
||||
|
||||
public void HideGame()
|
||||
{
|
||||
gameRoot.SetActive(true);
|
||||
SetBoardRootActive(false);
|
||||
}
|
||||
|
||||
public void ShowPause()
|
||||
{
|
||||
if (pauseRoot != null)
|
||||
{
|
||||
pauseRoot.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void HidePause()
|
||||
{
|
||||
if (pauseRoot != null)
|
||||
{
|
||||
pauseRoot.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowResult(GameState state)
|
||||
{
|
||||
if (resultRoot != null)
|
||||
{
|
||||
resultRoot.SetActive(true);
|
||||
}
|
||||
|
||||
if (resultText != null)
|
||||
{
|
||||
resultText.text = state == GameState.Won ? "YOU WIN" : "GAME OVER";
|
||||
}
|
||||
}
|
||||
|
||||
public void HideResult()
|
||||
{
|
||||
if (resultRoot != null)
|
||||
{
|
||||
resultRoot.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTimer(float seconds)
|
||||
{
|
||||
if (timerText != null)
|
||||
{
|
||||
timerText.text = Mathf.FloorToInt(seconds).ToString("00000");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMineCount(int minesCount)
|
||||
{
|
||||
if (mineText != null)
|
||||
{
|
||||
mineText.text = minesCount.ToString("00000");
|
||||
}
|
||||
}
|
||||
|
||||
public void BindConfig(MinesweeperUiConfig config)
|
||||
{
|
||||
uiConfig = config;
|
||||
}
|
||||
|
||||
public void RebuildBoard(IReadOnlyList<BoardCellData> cells, int width, int height, ICellViewFactory cellViewFactory)
|
||||
{
|
||||
ClearBoard();
|
||||
ConfigureGrid(width, height);
|
||||
|
||||
for (var i = 0; i < cells.Count; i++)
|
||||
{
|
||||
CreateCell(cells[i], cellViewFactory);
|
||||
}
|
||||
|
||||
RefreshBoard(cells);
|
||||
}
|
||||
|
||||
public void RefreshBoard(IReadOnlyList<BoardCellData> cells)
|
||||
{
|
||||
for (var i = 0; i < cells.Count; i++)
|
||||
{
|
||||
var cell = cells[i];
|
||||
if (cellsByCoordinate.TryGetValue(ToKey(cell.X, cell.Y), out var view))
|
||||
{
|
||||
view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBoardInputEnabled(bool enabled)
|
||||
{
|
||||
boardInputEnabled = enabled;
|
||||
foreach (var cell in cellsByCoordinate.Values)
|
||||
{
|
||||
cell.SetInputEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddButtonListeners()
|
||||
{
|
||||
if (pauseButton != null)
|
||||
{
|
||||
pauseButton.onClick.AddListener(OnPauseClicked);
|
||||
}
|
||||
|
||||
if (restartButton != null)
|
||||
{
|
||||
restartButton.onClick.AddListener(OnRestartClicked);
|
||||
}
|
||||
|
||||
if (resumeButton != null)
|
||||
{
|
||||
resumeButton.onClick.AddListener(OnResumeClicked);
|
||||
}
|
||||
|
||||
if (mainMenuButton != null)
|
||||
{
|
||||
mainMenuButton.onClick.AddListener(OnMainMenuClicked);
|
||||
}
|
||||
|
||||
if (resultRestartButton != null)
|
||||
{
|
||||
resultRestartButton.onClick.AddListener(OnRestartClicked);
|
||||
}
|
||||
|
||||
if (resultMainMenuButton != null)
|
||||
{
|
||||
resultMainMenuButton.onClick.AddListener(OnMainMenuClicked);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveButtonListeners()
|
||||
{
|
||||
if (pauseButton != null)
|
||||
{
|
||||
pauseButton.onClick.RemoveListener(OnPauseClicked);
|
||||
}
|
||||
|
||||
if (restartButton != null)
|
||||
{
|
||||
restartButton.onClick.RemoveListener(OnRestartClicked);
|
||||
}
|
||||
|
||||
if (resumeButton != null)
|
||||
{
|
||||
resumeButton.onClick.RemoveListener(OnResumeClicked);
|
||||
}
|
||||
|
||||
if (mainMenuButton != null)
|
||||
{
|
||||
mainMenuButton.onClick.RemoveListener(OnMainMenuClicked);
|
||||
}
|
||||
|
||||
if (resultRestartButton != null)
|
||||
{
|
||||
resultRestartButton.onClick.RemoveListener(OnRestartClicked);
|
||||
}
|
||||
|
||||
if (resultMainMenuButton != null)
|
||||
{
|
||||
resultMainMenuButton.onClick.RemoveListener(OnMainMenuClicked);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBoardRootActive(bool active)
|
||||
{
|
||||
if (boardPanel != null)
|
||||
{
|
||||
boardPanel.gameObject.SetActive(active);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureGrid(int width, int height)
|
||||
{
|
||||
if (gridLayoutGroup == null || boardPanel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
var parentRect = boardPanel.parent as RectTransform;
|
||||
var rect = parentRect != null ? parentRect.rect : boardPanel.rect;
|
||||
var panelWidth = rect.width > 0f ? rect.width : uiConfig.ReferenceCellSize * width;
|
||||
var panelHeight = rect.height > 0f ? rect.height : uiConfig.ReferenceCellSize * height;
|
||||
var cellSize = CalculateCellSize(panelWidth, panelHeight, width, height);
|
||||
var padding = cellSize * uiConfig.BoardPaddingRatio;
|
||||
var spacing = cellSize * uiConfig.GridSpacingRatio;
|
||||
|
||||
boardPanel.anchorMin = Vector2.zero;
|
||||
boardPanel.anchorMax = Vector2.one;
|
||||
boardPanel.offsetMin = new Vector2(padding, padding);
|
||||
boardPanel.offsetMax = new Vector2(-padding, -padding);
|
||||
|
||||
gridLayoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
|
||||
gridLayoutGroup.constraintCount = width;
|
||||
gridLayoutGroup.padding = new RectOffset();
|
||||
gridLayoutGroup.spacing = new Vector2(spacing, spacing);
|
||||
gridLayoutGroup.cellSize = new Vector2(cellSize, cellSize);
|
||||
currentPixelsPerUnitMultiplier = uiConfig.ReferenceCellSize / cellSize;
|
||||
}
|
||||
|
||||
private float CalculateCellSize(float panelWidth, float panelHeight, int width, int height)
|
||||
{
|
||||
var widthUnits = width + 2f * uiConfig.BoardPaddingRatio + Mathf.Max(0, width - 1) * uiConfig.GridSpacingRatio;
|
||||
var heightUnits = height + 2f * uiConfig.BoardPaddingRatio + Mathf.Max(0, height - 1) * uiConfig.GridSpacingRatio;
|
||||
var cellByWidth = panelWidth / widthUnits;
|
||||
var cellByHeight = panelHeight / heightUnits;
|
||||
return Mathf.Max(uiConfig.MinimumCellSize, Mathf.Floor(Mathf.Min(cellByWidth, cellByHeight)));
|
||||
}
|
||||
|
||||
private void CreateCell(BoardCellData cell, ICellViewFactory cellViewFactory)
|
||||
{
|
||||
var view = cellViewFactory.CreateCell(cell, gridLayoutGroup.transform);
|
||||
view.SetInputEnabled(boardInputEnabled);
|
||||
view.OpenRequested += OnCellOpenRequested;
|
||||
view.FlagRequested += OnCellFlagRequested;
|
||||
view.PressStarted += OnCellPressStarted;
|
||||
view.PressEnded += OnCellPressEnded;
|
||||
cellsByCoordinate[ToKey(cell.X, cell.Y)] = view;
|
||||
}
|
||||
|
||||
private void ClearBoard()
|
||||
{
|
||||
foreach (var cell in cellsByCoordinate.Values)
|
||||
{
|
||||
if (cell != null)
|
||||
{
|
||||
cell.OpenRequested -= OnCellOpenRequested;
|
||||
cell.FlagRequested -= OnCellFlagRequested;
|
||||
cell.PressStarted -= OnCellPressStarted;
|
||||
cell.PressEnded -= OnCellPressEnded;
|
||||
}
|
||||
}
|
||||
|
||||
cellsByCoordinate.Clear();
|
||||
|
||||
if (gridLayoutGroup == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = gridLayoutGroup.transform.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(gridLayoutGroup.transform.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCellOpenRequested(int x, int y)
|
||||
{
|
||||
CellOpenRequested?.Invoke(x, y);
|
||||
}
|
||||
|
||||
private void OnCellFlagRequested(int x, int y)
|
||||
{
|
||||
CellFlagRequested?.Invoke(x, y);
|
||||
}
|
||||
|
||||
private void OnCellPressStarted()
|
||||
{
|
||||
CellPressStarted?.Invoke();
|
||||
}
|
||||
|
||||
private void OnCellPressEnded()
|
||||
{
|
||||
CellPressEnded?.Invoke();
|
||||
}
|
||||
|
||||
private void OnPauseClicked()
|
||||
{
|
||||
PauseRequested?.Invoke();
|
||||
}
|
||||
|
||||
private void OnRestartClicked()
|
||||
{
|
||||
RestartRequested?.Invoke();
|
||||
}
|
||||
|
||||
private void OnResumeClicked()
|
||||
{
|
||||
ResumeRequested?.Invoke();
|
||||
}
|
||||
|
||||
private void OnMainMenuClicked()
|
||||
{
|
||||
GoToMenuRequested?.Invoke();
|
||||
}
|
||||
|
||||
private static int ToKey(int x, int y)
|
||||
{
|
||||
return (y << 16) ^ x;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c906a10872edd04480e534703fc4fea
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.Presentation.Factories;
|
||||
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
public interface IGameView : IView
|
||||
{
|
||||
event Action RestartRequested;
|
||||
event Action GoToMenuRequested;
|
||||
event Action PauseRequested;
|
||||
event Action ResumeRequested;
|
||||
event Action CellPressStarted;
|
||||
event Action CellPressEnded;
|
||||
event Action<int, int> CellOpenRequested;
|
||||
event Action<int, int> CellFlagRequested;
|
||||
|
||||
void ShowGame();
|
||||
void HideGame();
|
||||
void ShowPause();
|
||||
void HidePause();
|
||||
void ShowResult(GameState state);
|
||||
void HideResult();
|
||||
void SetMineCount(int minesCount);
|
||||
void SetTimer(float seconds);
|
||||
void RebuildBoard(IReadOnlyList<BoardCellData> cells, int width, int height, ICellViewFactory cellViewFactory);
|
||||
void RefreshBoard(IReadOnlyList<BoardCellData> cells);
|
||||
void SetBoardInputEnabled(bool enabled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8c5423ea37354a4e82b05aadfbf239f
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
public interface IMainMenuView : IView
|
||||
{
|
||||
event Action StartClicked;
|
||||
|
||||
void Show();
|
||||
void Hide();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66cad179080f23a479c3418932137653
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using Minesweeper.Config;
|
||||
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
public interface ITopPanelView : IView
|
||||
{
|
||||
event Action SmileClicked;
|
||||
|
||||
void SetActive(bool active);
|
||||
void SetRemainingMines(int remainingMines);
|
||||
void SetTimer(float seconds);
|
||||
void SetSmile(SmileFaceState state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0c8cae3d1ad4d7a9ca3e2f8f435cd7b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
public interface IView
|
||||
{
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user