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 { private const float ResizeRefreshDelaySeconds = 0.5f; private const float ResizeSizeEpsilon = 0.5f; [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 cellsByCoordinate = new Dictionary(); private IReadOnlyList currentCells; private bool boardInputEnabled = true; private bool currentRevealUnflaggedMines; private bool resizeRefreshPending; private int currentBoardWidth; private int currentBoardHeight; private float currentPixelsPerUnitMultiplier = 1f; private float resizeStableAt; private Vector2 lastObservedLayoutSize; 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 CellOpenRequested; public event Action CellFlagRequested; private void Awake() { if (gameRoot == null) { gameRoot = gameObject; } } private void OnEnable() { AddButtonListeners(); } private void OnDisable() { RemoveButtonListeners(); } private void Update() { TrackBoardResize(); } 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 BindScreens(MinesweeperScreenRefs refs) { if (isActiveAndEnabled) { RemoveButtonListeners(); } boardPanel = refs.BoardPanel; gridLayoutGroup = refs.BoardGrid; pauseRoot = refs.PauseRoot; restartButton = refs.PauseRestartButton; resumeButton = refs.PauseResumeButton; mainMenuButton = refs.PauseMainMenuButton; resultRoot = refs.ResultRoot; resultRestartButton = refs.ResultRestartButton; resultMainMenuButton = refs.ResultMainMenuButton; resultText = refs.ResultText; ResetResizeTracking(); if (isActiveAndEnabled) { AddButtonListeners(); } } public void RebuildBoard(IReadOnlyList cells, int width, int height, ICellViewFactory cellViewFactory, bool revealUnflaggedMines) { ClearBoard(); currentBoardWidth = width; currentBoardHeight = height; currentCells = cells; currentRevealUnflaggedMines = revealUnflaggedMines; ConfigureGrid(width, height); for (var i = 0; i < cells.Count; i++) { CreateCell(cells[i], cellViewFactory); } RefreshBoard(cells, revealUnflaggedMines); } public void RefreshBoard(IReadOnlyList cells, bool revealUnflaggedMines) { currentCells = cells; currentRevealUnflaggedMines = revealUnflaggedMines; 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, revealUnflaggedMines); } } } 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); } if (active) { ResetResizeTracking(); } } private void TrackBoardResize() { if (boardPanel == null || !boardPanel.gameObject.activeInHierarchy || currentCells == null || currentBoardWidth <= 0 || currentBoardHeight <= 0) { return; } var layoutSize = GetLayoutSourceSize(); if (layoutSize.x <= 0f || layoutSize.y <= 0f) { return; } if (HasSizeChanged(layoutSize, lastObservedLayoutSize)) { lastObservedLayoutSize = layoutSize; resizeStableAt = Time.unscaledTime + ResizeRefreshDelaySeconds; resizeRefreshPending = true; return; } if (resizeRefreshPending && Time.unscaledTime >= resizeStableAt) { resizeRefreshPending = false; RefreshBoardLayout(); } } private void RefreshBoardLayout() { ConfigureGrid(currentBoardWidth, currentBoardHeight); RefreshBoard(currentCells, currentRevealUnflaggedMines); } private void ConfigureGrid(int width, int height) { if (gridLayoutGroup == null || boardPanel == null) { return; } Canvas.ForceUpdateCanvases(); var layoutSize = GetLayoutSourceSize(); var panelWidth = layoutSize.x > 0f ? layoutSize.x : uiConfig.ReferenceCellSize * width; var panelHeight = layoutSize.y > 0f ? layoutSize.y : 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; lastObservedLayoutSize = layoutSize; } private Vector2 GetLayoutSourceSize() { var parentRect = boardPanel.parent as RectTransform; var rect = parentRect != null ? parentRect.rect : boardPanel.rect; return rect.size; } private void ResetResizeTracking() { resizeRefreshPending = false; if (boardPanel != null) { lastObservedLayoutSize = GetLayoutSourceSize(); } } private static bool HasSizeChanged(Vector2 current, Vector2 previous) { return Mathf.Abs(current.x - previous.x) > ResizeSizeEpsilon || Mathf.Abs(current.y - previous.y) > ResizeSizeEpsilon; } 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; } } }