[Fix] UI Logic

This commit is contained in:
2026-06-06 22:33:15 +07:00
parent f4ecf8b6f9
commit fdb22e9213
134 changed files with 5367 additions and 269 deletions
@@ -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
+8
View File
@@ -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
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 66a8b79ae5812744680f9e386b007144
@@ -0,0 +1,53 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace Minesweeper.Presentation.Views
{
public sealed class MainMenuView : MonoBehaviour, IMainMenuView
{
[SerializeField] private GameObject root;
[SerializeField] private Button startButton;
public event Action StartClicked;
private void Awake()
{
if (root == null)
{
root = gameObject;
}
}
private void OnEnable()
{
if (startButton != null)
{
startButton.onClick.AddListener(OnStartClicked);
}
}
private void OnDisable()
{
if (startButton != null)
{
startButton.onClick.RemoveListener(OnStartClicked);
}
}
public void Show()
{
root.SetActive(true);
}
public void Hide()
{
root.SetActive(false);
}
private void OnStartClicked()
{
StartClicked?.Invoke();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bb899c7e47cd4e341b0258dac3f7a238
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using Minesweeper.Core;
using Minesweeper.Presentation.Factories;
namespace Minesweeper.Presentation.Views
{
public sealed class NullGameView : IGameView
{
public event Action RestartRequested
{
add { }
remove { }
}
public event Action GoToMenuRequested
{
add { }
remove { }
}
public event Action PauseRequested
{
add { }
remove { }
}
public event Action ResumeRequested
{
add { }
remove { }
}
public event Action CellPressStarted
{
add { }
remove { }
}
public event Action CellPressEnded
{
add { }
remove { }
}
public event Action<int, int> CellOpenRequested
{
add { }
remove { }
}
public event Action<int, int> CellFlagRequested
{
add { }
remove { }
}
public void ShowGame()
{
}
public void HideGame()
{
}
public void ShowPause()
{
}
public void HidePause()
{
}
public void ShowResult(GameState state)
{
}
public void HideResult()
{
}
public void SetMineCount(int minesCount)
{
}
public void SetTimer(float seconds)
{
}
public void RebuildBoard(IReadOnlyList<BoardCellData> cells, int width, int height, ICellViewFactory cellViewFactory)
{
}
public void RefreshBoard(IReadOnlyList<BoardCellData> cells)
{
}
public void SetBoardInputEnabled(bool enabled)
{
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c800a42df535f9347bea10f164fd2e15
@@ -0,0 +1,21 @@
using System;
namespace Minesweeper.Presentation.Views
{
public sealed class NullMainMenuView : IMainMenuView
{
public event Action StartClicked
{
add { }
remove { }
}
public void Show()
{
}
public void Hide()
{
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6b41dd95488a1db42adccef0225d2f89
@@ -0,0 +1,30 @@
using System;
using Minesweeper.Config;
namespace Minesweeper.Presentation.Views
{
public sealed class NullTopPanelView : ITopPanelView
{
public event Action SmileClicked
{
add { }
remove { }
}
public void SetActive(bool active)
{
}
public void SetRemainingMines(int remainingMines)
{
}
public void SetTimer(float seconds)
{
}
public void SetSmile(SmileFaceState state)
{
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c8372ef8aa2747d7af12fce4f1324baf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
using System;
using Minesweeper.Config;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Minesweeper.Presentation.Views
{
public sealed class TopPanelView : MonoBehaviour, ITopPanelView
{
[SerializeField] private GameObject root;
[SerializeField] private TMP_Text mineText;
[SerializeField] private TMP_Text timerText;
[SerializeField] private Button smileButton;
[SerializeField] private Image smileImage;
[SerializeField] private MinesweeperUiConfig uiConfig;
public event Action SmileClicked;
private void Awake()
{
if (root == null)
{
root = gameObject;
}
}
private void OnEnable()
{
if (smileButton != null)
{
smileButton.onClick.AddListener(OnSmileClicked);
}
}
private void OnDisable()
{
if (smileButton != null)
{
smileButton.onClick.RemoveListener(OnSmileClicked);
}
}
public void SetActive(bool active)
{
root.SetActive(active);
}
public void SetRemainingMines(int remainingMines)
{
if (mineText != null)
{
mineText.text = Mathf.Max(0, remainingMines).ToString("00000");
}
}
public void SetTimer(float seconds)
{
if (timerText != null)
{
timerText.text = Mathf.FloorToInt(seconds).ToString("00000");
}
}
public void SetSmile(SmileFaceState state)
{
if (smileImage == null || uiConfig == null)
{
return;
}
var sprite = uiConfig.GetSmileSprite(state);
if (sprite != null)
{
smileImage.sprite = sprite;
}
smileImage.color = Color.white;
}
public void BindConfig(MinesweeperUiConfig config)
{
uiConfig = config;
}
private void OnSmileClicked()
{
SmileClicked?.Invoke();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb4efc57f8404c98bff81ed5db093d81
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: