diff --git a/Assets/Minesweeper/Runtime/Commands/GameCommandHandlers.cs b/Assets/Minesweeper/Runtime/Commands/GameCommandHandlers.cs index d150c86..e885282 100644 --- a/Assets/Minesweeper/Runtime/Commands/GameCommandHandlers.cs +++ b/Assets/Minesweeper/Runtime/Commands/GameCommandHandlers.cs @@ -22,17 +22,23 @@ namespace Minesweeper.Commands { 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, IGameStateService gameStateService) + 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); @@ -45,16 +51,23 @@ namespace Minesweeper.Commands private readonly IBoardEcsSyncService boardEcsSyncService; private readonly IBoardService boardService; private readonly IGameStateService gameStateService; + private readonly IGamePauseService pauseService; - public OpenCellCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService) + 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) { @@ -103,16 +116,23 @@ namespace Minesweeper.Commands private readonly IBoardEcsSyncService boardEcsSyncService; private readonly IBoardService boardService; private readonly IGameStateService gameStateService; + private readonly IGamePauseService pauseService; - public ToggleFlagCommandHandler(IBoardService boardService, IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService) + 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) { @@ -134,17 +154,23 @@ namespace Minesweeper.Commands { 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, IGameStateService gameStateService) + 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); @@ -154,31 +180,58 @@ namespace Minesweeper.Commands public sealed class PauseCommandHandler : IGameCommandHandler { + 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 { + private readonly IGamePauseService pauseService; + + public ResumeCommandHandler(IGamePauseService pauseService) + { + this.pauseService = pauseService; + } + public void Handle(ResumeCommand command) { + pauseService.Resume(); } } public sealed class GoToMenuCommandHandler : IGameCommandHandler { private readonly IBoardEcsSyncService boardEcsSyncService; + private readonly IGamePauseService pauseService; private readonly IGameStateService gameStateService; + private readonly IGameTimerService timerService; - public GoToMenuCommandHandler(IBoardEcsSyncService boardEcsSyncService, IGameStateService gameStateService) + 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); } diff --git a/Assets/Minesweeper/Runtime/Core/GamePauseService.cs b/Assets/Minesweeper/Runtime/Core/GamePauseService.cs new file mode 100644 index 0000000..de782e2 --- /dev/null +++ b/Assets/Minesweeper/Runtime/Core/GamePauseService.cs @@ -0,0 +1,33 @@ +using System; + +namespace Minesweeper.Core +{ + public sealed class GamePauseService : IGamePauseService + { + public event Action 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); + } + } +} diff --git a/Assets/Minesweeper/Runtime/Core/GamePauseService.cs.meta b/Assets/Minesweeper/Runtime/Core/GamePauseService.cs.meta new file mode 100644 index 0000000..999a8ac --- /dev/null +++ b/Assets/Minesweeper/Runtime/Core/GamePauseService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 40441c28481279147959eafabd8a032a \ No newline at end of file diff --git a/Assets/Minesweeper/Runtime/Core/GameTimerService.cs b/Assets/Minesweeper/Runtime/Core/GameTimerService.cs new file mode 100644 index 0000000..297a6a0 --- /dev/null +++ b/Assets/Minesweeper/Runtime/Core/GameTimerService.cs @@ -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 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); + } + } +} diff --git a/Assets/Minesweeper/Runtime/Core/GameTimerService.cs.meta b/Assets/Minesweeper/Runtime/Core/GameTimerService.cs.meta new file mode 100644 index 0000000..599e9f3 --- /dev/null +++ b/Assets/Minesweeper/Runtime/Core/GameTimerService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ed8262be24a32a04abfd5bc5ec8544bb \ No newline at end of file diff --git a/Assets/Minesweeper/Runtime/Core/IGamePauseService.cs b/Assets/Minesweeper/Runtime/Core/IGamePauseService.cs new file mode 100644 index 0000000..29c6f1a --- /dev/null +++ b/Assets/Minesweeper/Runtime/Core/IGamePauseService.cs @@ -0,0 +1,14 @@ +using System; + +namespace Minesweeper.Core +{ + public interface IGamePauseService + { + event Action PauseChanged; + + bool IsPaused { get; } + + void Pause(); + void Resume(); + } +} diff --git a/Assets/Minesweeper/Runtime/Core/IGamePauseService.cs.meta b/Assets/Minesweeper/Runtime/Core/IGamePauseService.cs.meta new file mode 100644 index 0000000..f6afc47 --- /dev/null +++ b/Assets/Minesweeper/Runtime/Core/IGamePauseService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 82dfb9fe1e7004f4f88df366f8e76b2d \ No newline at end of file diff --git a/Assets/Minesweeper/Runtime/Core/IGameTimerService.cs b/Assets/Minesweeper/Runtime/Core/IGameTimerService.cs new file mode 100644 index 0000000..ba89d37 --- /dev/null +++ b/Assets/Minesweeper/Runtime/Core/IGameTimerService.cs @@ -0,0 +1,13 @@ +using System; + +namespace Minesweeper.Core +{ + public interface IGameTimerService + { + event Action TimeChanged; + + float ElapsedSeconds { get; } + + void Reset(); + } +} diff --git a/Assets/Minesweeper/Runtime/Core/IGameTimerService.cs.meta b/Assets/Minesweeper/Runtime/Core/IGameTimerService.cs.meta new file mode 100644 index 0000000..c7e0f79 --- /dev/null +++ b/Assets/Minesweeper/Runtime/Core/IGameTimerService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 61242d395cb1d974daffd9e0815ec34c \ No newline at end of file diff --git a/Assets/Minesweeper/Runtime/Infrastructure/MinesweeperLifetimeScope.cs b/Assets/Minesweeper/Runtime/Infrastructure/MinesweeperLifetimeScope.cs index ae0cc72..e08523f 100644 --- a/Assets/Minesweeper/Runtime/Infrastructure/MinesweeperLifetimeScope.cs +++ b/Assets/Minesweeper/Runtime/Infrastructure/MinesweeperLifetimeScope.cs @@ -16,18 +16,38 @@ namespace Minesweeper.Infrastructure public sealed class MinesweeperLifetimeScope : LifetimeScope { [SerializeField] private MinesweeperGameConfig gameConfig; + [SerializeField] private MainMenuView mainMenuView; + [SerializeField] private GameView gameView; protected override void Configure(IContainerBuilder builder) { builder.RegisterInstance(GetConfig()); builder.Register(Lifetime.Singleton).As(); builder.Register(Lifetime.Singleton).As(); + builder.Register(Lifetime.Singleton).As(); + builder.Register(Lifetime.Singleton).As().As(); builder.Register(Lifetime.Singleton).As(); builder.Register(Lifetime.Singleton).As(); builder.Register(Lifetime.Singleton).As(); builder.Register(Lifetime.Singleton).As(); - builder.Register(Lifetime.Singleton).As(); - builder.Register(Lifetime.Singleton).As(); + + if (mainMenuView != null) + { + builder.RegisterComponent(mainMenuView).As(); + } + else + { + builder.Register(Lifetime.Singleton).As(); + } + + if (gameView != null) + { + builder.RegisterComponent(gameView).As(); + } + else + { + builder.Register(Lifetime.Singleton).As(); + } builder.Register(Lifetime.Singleton); builder.Register(Lifetime.Singleton); diff --git a/Assets/Minesweeper/Runtime/Presentation/Presenters/GamePresenter.cs b/Assets/Minesweeper/Runtime/Presentation/Presenters/GamePresenter.cs index 2bcca81..14b5de4 100644 --- a/Assets/Minesweeper/Runtime/Presentation/Presenters/GamePresenter.cs +++ b/Assets/Minesweeper/Runtime/Presentation/Presenters/GamePresenter.cs @@ -1,4 +1,5 @@ using Minesweeper.Commands; +using Minesweeper.Core; using Minesweeper.Presentation.ReadModels; using Minesweeper.Presentation.Views; @@ -7,24 +8,39 @@ namespace Minesweeper.Presentation.Presenters public sealed class GamePresenter : IPresenter { private readonly IGameCommandDispatcher commandDispatcher; + private readonly IGamePauseService pauseService; private readonly IGameReadModel readModel; + private readonly IGameStateService gameStateService; + private readonly IGameTimerService timerService; private readonly IGameView view; + private bool boardBuilt; - public GamePresenter(IGameCommandDispatcher commandDispatcher, IGameReadModel readModel, IGameView view = null) + public GamePresenter(IGameCommandDispatcher commandDispatcher, IGamePauseService pauseService, IGameReadModel readModel, IGameStateService gameStateService, IGameTimerService timerService, IGameView view = null) { this.commandDispatcher = commandDispatcher; + this.pauseService = pauseService; this.readModel = readModel; + this.gameStateService = gameStateService; + this.timerService = timerService; this.view = view; } public void Initialize() { - _ = readModel.State; - if (view != null) { view.RestartRequested += OnRestartRequested; view.GoToMenuRequested += OnGoToMenuRequested; + view.PauseRequested += OnPauseRequested; + view.ResumeRequested += OnResumeRequested; + view.CellOpenRequested += OnCellOpenRequested; + view.CellFlagRequested += OnCellFlagRequested; + gameStateService.StateChanged += OnStateChanged; + pauseService.PauseChanged += OnPauseChanged; + timerService.TimeChanged += OnTimeChanged; + OnStateChanged(gameStateService.Current); + OnPauseChanged(pauseService.IsPaused); + OnTimeChanged(timerService.ElapsedSeconds); } } @@ -34,17 +50,112 @@ namespace Minesweeper.Presentation.Presenters { view.RestartRequested -= OnRestartRequested; view.GoToMenuRequested -= OnGoToMenuRequested; + view.PauseRequested -= OnPauseRequested; + view.ResumeRequested -= OnResumeRequested; + view.CellOpenRequested -= OnCellOpenRequested; + view.CellFlagRequested -= OnCellFlagRequested; + gameStateService.StateChanged -= OnStateChanged; + pauseService.PauseChanged -= OnPauseChanged; + timerService.TimeChanged -= OnTimeChanged; } } private void OnRestartRequested() { commandDispatcher.Dispatch(new RestartCommand()); + RebuildBoard(); } 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) + { + commandDispatcher.Dispatch(new OpenCellCommand(x, y)); + RefreshBoard(); + UpdateBoardInput(); + } + + private void OnCellFlagRequested(int x, int y) + { + commandDispatcher.Dispatch(new ToggleFlagCommand(x, y)); + RefreshBoard(); + UpdateBoardInput(); + } + + private void OnStateChanged(GameState state) + { + if (state == GameState.FieldSelection) + { + boardBuilt = false; + view.HideGame(); + view.HidePause(); + return; + } + + view.ShowGame(); + + 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); + boardBuilt = true; + 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)); + } } } diff --git a/Assets/Minesweeper/Runtime/Presentation/Presenters/MainMenuPresenter.cs b/Assets/Minesweeper/Runtime/Presentation/Presenters/MainMenuPresenter.cs index 8ad2255..78a9b7a 100644 --- a/Assets/Minesweeper/Runtime/Presentation/Presenters/MainMenuPresenter.cs +++ b/Assets/Minesweeper/Runtime/Presentation/Presenters/MainMenuPresenter.cs @@ -1,4 +1,5 @@ using Minesweeper.Commands; +using Minesweeper.Core; using Minesweeper.Presentation.Views; namespace Minesweeper.Presentation.Presenters @@ -6,11 +7,13 @@ 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, IMainMenuView view = null) + public MainMenuPresenter(IGameCommandDispatcher commandDispatcher, IGameStateService gameStateService, IMainMenuView view = null) { this.commandDispatcher = commandDispatcher; + this.gameStateService = gameStateService; this.view = view; } @@ -19,6 +22,8 @@ namespace Minesweeper.Presentation.Presenters if (view != null) { view.StartClicked += OnStartClicked; + gameStateService.StateChanged += OnStateChanged; + OnStateChanged(gameStateService.Current); } } @@ -27,6 +32,7 @@ namespace Minesweeper.Presentation.Presenters if (view != null) { view.StartClicked -= OnStartClicked; + gameStateService.StateChanged -= OnStateChanged; } } @@ -34,5 +40,17 @@ namespace Minesweeper.Presentation.Presenters { commandDispatcher.Dispatch(new StartGameCommand()); } + + private void OnStateChanged(GameState state) + { + if (state == GameState.FieldSelection) + { + view.Show(); + } + else + { + view.Hide(); + } + } } } diff --git a/Assets/Minesweeper/Runtime/Presentation/Views/CellView.cs b/Assets/Minesweeper/Runtime/Presentation/Views/CellView.cs new file mode 100644 index 0000000..f432486 --- /dev/null +++ b/Assets/Minesweeper/Runtime/Presentation/Views/CellView.cs @@ -0,0 +1,93 @@ +using System; +using Minesweeper.Core; +using TMPro; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace Minesweeper.Presentation.Views +{ + public sealed class CellView : MonoBehaviour, IPointerClickHandler + { + [SerializeField] private Button button; + [SerializeField] private Image image; + [SerializeField] private TMP_Text label; + + private int x; + private int y; + private bool inputEnabled = true; + + public event Action OpenRequested; + public event Action FlagRequested; + + public void Bind(Button button, Image image, TMP_Text label) + { + this.button = button; + this.image = image; + this.label = label; + } + + 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; + } + } + + public void Render(BoardCellData cell, float pixelsPerUnitMultiplier) + { + gameObject.name = $"bt_{cell.X}_{cell.Y}_{cell.DisplayValue}"; + + if (image != null) + { + image.pixelsPerUnitMultiplier = pixelsPerUnitMultiplier; + image.color = cell.IsOpened ? new Color(0.78f, 0.78f, 0.78f) : Color.white; + } + + if (label != null) + { + if (cell.IsFlagged) + { + label.text = "F"; + } + else if (!cell.IsOpened) + { + label.text = string.Empty; + } + else if (cell.IsMine) + { + label.text = "M"; + } + else + { + label.text = cell.NeighborMines == 0 ? string.Empty : cell.NeighborMines.ToString(); + } + } + } + + 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); + } + } + } +} diff --git a/Assets/Minesweeper/Runtime/Presentation/Views/CellView.cs.meta b/Assets/Minesweeper/Runtime/Presentation/Views/CellView.cs.meta new file mode 100644 index 0000000..8051ec8 --- /dev/null +++ b/Assets/Minesweeper/Runtime/Presentation/Views/CellView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2904d462d22809c499afe1842f6e6239 \ No newline at end of file diff --git a/Assets/Minesweeper/Runtime/Presentation/Views/GameView.cs b/Assets/Minesweeper/Runtime/Presentation/Views/GameView.cs new file mode 100644 index 0000000..c47c57b --- /dev/null +++ b/Assets/Minesweeper/Runtime/Presentation/Views/GameView.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using Minesweeper.Core; +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 RectTransform boardPanel; + [SerializeField] private GridLayoutGroup gridLayoutGroup; + [SerializeField] private Button pauseButton; + [SerializeField] private Button restartButton; + [SerializeField] private Button resumeButton; + [SerializeField] private Button mainMenuButton; + [SerializeField] private TMP_Text timerText; + [SerializeField] private TMP_Text mineText; + [SerializeField] private float spacing = 2f; + [SerializeField] private float basePixelsPerUnitCellSize = 32f; + + private readonly Dictionary cellsByCoordinate = new Dictionary(); + 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 CellOpenRequested; + public event Action CellFlagRequested; + + private void Awake() + { + if (gameRoot == null) + { + gameRoot = gameObject; + } + } + + private void OnEnable() + { + AddButtonListeners(); + } + + private void OnDisable() + { + RemoveButtonListeners(); + } + + public void ShowGame() + { + gameRoot.SetActive(true); + } + + public void HideGame() + { + gameRoot.SetActive(false); + } + + public void ShowPause() + { + if (pauseRoot != null) + { + pauseRoot.SetActive(true); + } + } + + public void HidePause() + { + if (pauseRoot != null) + { + pauseRoot.SetActive(false); + } + } + + public void SetTimer(float seconds) + { + if (timerText != null) + { + timerText.text = Mathf.FloorToInt(seconds).ToString("000"); + } + } + + public void SetMineCount(int minesCount) + { + if (mineText != null) + { + mineText.text = minesCount.ToString("000"); + } + } + + public void RebuildBoard(IReadOnlyList cells, int width, int height) + { + ClearBoard(); + ConfigureGrid(width, height); + + for (var i = 0; i < cells.Count; i++) + { + CreateCell(cells[i]); + } + + RefreshBoard(cells); + } + + public void RefreshBoard(IReadOnlyList 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, 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); + } + } + + 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); + } + } + + private void ConfigureGrid(int width, int height) + { + if (gridLayoutGroup == null || boardPanel == null) + { + return; + } + + Canvas.ForceUpdateCanvases(); + + var rect = boardPanel.rect; + var panelWidth = rect.width > 0f ? rect.width : 512f; + var panelHeight = rect.height > 0f ? rect.height : 512f; + var padding = gridLayoutGroup.padding; + var availableWidth = panelWidth - padding.left - padding.right - spacing * Mathf.Max(0, width - 1); + var availableHeight = panelHeight - padding.top - padding.bottom - spacing * Mathf.Max(0, height - 1); + var cellSize = Mathf.Floor(Mathf.Min(availableWidth / width, availableHeight / height)); + cellSize = Mathf.Max(8f, cellSize); + + gridLayoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount; + gridLayoutGroup.constraintCount = width; + gridLayoutGroup.spacing = new Vector2(spacing, spacing); + gridLayoutGroup.cellSize = new Vector2(cellSize, cellSize); + currentPixelsPerUnitMultiplier = Mathf.Clamp(basePixelsPerUnitCellSize / cellSize, 0.25f, 4f); + } + + private void CreateCell(BoardCellData cell) + { + var go = new GameObject($"bt_{cell.X}_{cell.Y}_{cell.DisplayValue}", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button)); + go.transform.SetParent(gridLayoutGroup.transform, false); + + var labelGo = new GameObject("Text", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI)); + labelGo.transform.SetParent(go.transform, false); + var labelRect = (RectTransform)labelGo.transform; + labelRect.anchorMin = Vector2.zero; + labelRect.anchorMax = Vector2.one; + labelRect.offsetMin = Vector2.zero; + labelRect.offsetMax = Vector2.zero; + + var label = labelGo.GetComponent(); + label.alignment = TextAlignmentOptions.Center; + label.enableAutoSizing = true; + label.fontSizeMin = 6f; + label.fontSizeMax = 32f; + label.color = Color.black; + + var view = go.AddComponent(); + view.Bind(go.GetComponent