[Add] Menu with configs and size fix
This commit is contained in:
@@ -12,7 +12,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: b4e8d5c36f36bb443b640a85df3e7077, type: 3}
|
||||
m_Name: MinesweeperGameConfig
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Minesweeper.Config.MinesweeperGameConfig
|
||||
<Width>k__BackingField: 9
|
||||
<Height>k__BackingField: 9
|
||||
<MinesCount>k__BackingField: 1
|
||||
<RestartKey>k__BackingField: 114
|
||||
<MinSizeX>k__BackingField: 2
|
||||
<MaxSizeX>k__BackingField: 50
|
||||
<MinSizeY>k__BackingField: 2
|
||||
<MaxSizeY>k__BackingField: 50
|
||||
|
||||
@@ -35,7 +35,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -15, y: -15}
|
||||
m_SizeDelta: {x: -8, y: -8}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &3798432596530835849
|
||||
GameObject:
|
||||
@@ -162,7 +162,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 30, y: 30}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4574882275002937660
|
||||
CanvasRenderer:
|
||||
@@ -219,12 +219,12 @@ MonoBehaviour:
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 54.6
|
||||
m_fontSize: 35.8
|
||||
m_fontSizeBase: 15
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 1
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontSizeMin: 1
|
||||
m_fontSizeMax: 300
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
@@ -258,7 +258,7 @@ MonoBehaviour:
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 2, y: 2, z: 2, w: 2}
|
||||
m_margin: {x: 1, y: 1, z: 1, w: 1}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
|
||||
@@ -428,7 +428,7 @@ GameObject:
|
||||
- component: {fileID: 2154271443323945884}
|
||||
- component: {fileID: 6204093840248163070}
|
||||
m_Layer: 5
|
||||
m_Name: Text (Mine) (1)
|
||||
m_Name: Text (Max)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -722,10 +722,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6299792064077961574}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 1}
|
||||
m_AnchorMax: {x: 0.5, y: 1}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 250, y: 48}
|
||||
m_SizeDelta: {x: 0, y: 48}
|
||||
m_Pivot: {x: 0.5, y: 0}
|
||||
--- !u!222 &7925194886356738603
|
||||
CanvasRenderer:
|
||||
@@ -996,10 +996,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4947738170035721252}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 1}
|
||||
m_AnchorMax: {x: 0.5, y: 1}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 250, y: 48}
|
||||
m_SizeDelta: {x: 0, y: 48}
|
||||
m_Pivot: {x: 0.5, y: 0}
|
||||
--- !u!222 &4764443876518541376
|
||||
CanvasRenderer:
|
||||
@@ -1208,10 +1208,10 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 8549637007489853463}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 1}
|
||||
m_AnchorMax: {x: 0.5, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 250, y: 48}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 1}
|
||||
m_SizeDelta: {x: 0, y: 48}
|
||||
m_Pivot: {x: 0.5, y: 0}
|
||||
--- !u!222 &929277193634291900
|
||||
CanvasRenderer:
|
||||
@@ -1421,8 +1421,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: -391, y: -192}
|
||||
m_SizeDelta: {x: -20, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &3877081085258266573
|
||||
GameObject:
|
||||
@@ -1609,7 +1609,7 @@ GameObject:
|
||||
- component: {fileID: 7567565223110830849}
|
||||
- component: {fileID: 3264166992253475585}
|
||||
m_Layer: 5
|
||||
m_Name: Text (Mine)
|
||||
m_Name: Text (Min)
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -2028,8 +2028,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: -391, y: -192}
|
||||
m_SizeDelta: {x: -20, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &6455657920195163342
|
||||
GameObject:
|
||||
@@ -2068,7 +2068,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -21}
|
||||
m_SizeDelta: {x: -120, y: -162}
|
||||
m_SizeDelta: {x: -150, y: -162}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &6446935312079034905
|
||||
MonoBehaviour:
|
||||
@@ -2420,8 +2420,8 @@ MonoBehaviour:
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 57
|
||||
m_fontSizeBase: 57
|
||||
m_fontSize: 54
|
||||
m_fontSizeBase: 54
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
@@ -2664,6 +2664,24 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Minesweeper.Presentation.Views.MainMenuView
|
||||
root: {fileID: 7682962739562644362}
|
||||
startButton: {fileID: 3904309382312306706}
|
||||
sizeXSlider:
|
||||
slider: {fileID: 2334871180416613717}
|
||||
minText: {fileID: 4504773017523582976}
|
||||
maxText: {fileID: 6866883084726833735}
|
||||
valueText: {fileID: 4474316482196456833}
|
||||
valueLabel: 'Size X:'
|
||||
sizeYSlider:
|
||||
slider: {fileID: 4051923562216906313}
|
||||
minText: {fileID: 1453579952739003993}
|
||||
maxText: {fileID: 8013579874201281113}
|
||||
valueText: {fileID: 8926983590126121119}
|
||||
valueLabel: 'Size Y:'
|
||||
minesSlider:
|
||||
slider: {fileID: 6456819381917595383}
|
||||
minText: {fileID: 3264166992253475585}
|
||||
maxText: {fileID: 6204093840248163070}
|
||||
valueText: {fileID: 7868341748139096968}
|
||||
valueLabel: 'Mines count:'
|
||||
--- !u!1 &7723311763242053893
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2847,8 +2865,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: -391, y: -192}
|
||||
m_SizeDelta: {x: -20, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &8648434963355287167
|
||||
GameObject:
|
||||
|
||||
@@ -5,11 +5,11 @@ namespace Minesweeper.Config
|
||||
[CreateAssetMenu(fileName = "MinesweeperGameConfig", menuName = "Minesweeper/Game Config")]
|
||||
public sealed class MinesweeperGameConfig : ScriptableObject
|
||||
{
|
||||
[field: SerializeField, Min(1)] public int Width { get; private set; } = 9;
|
||||
[field: SerializeField, Min(1)] public int Height { get; private set; } = 9;
|
||||
[field: SerializeField, Min(1)] public int MinesCount { get; private set; } = 10;
|
||||
[field: SerializeField] public KeyCode RestartKey { get; private set; } = KeyCode.R;
|
||||
[field: SerializeField, Min(1)] public int MinSizeX { get; private set; } = 2;
|
||||
[field: SerializeField, Min(1)] public int MaxSizeX { get; private set; } = 50;
|
||||
[field: SerializeField, Min(1)] public int MinSizeY { get; private set; } = 2;
|
||||
[field: SerializeField, Min(1)] public int MaxSizeY { get; private set; } = 50;
|
||||
|
||||
public bool IsValid => Width > 0 && Height > 0 && MinesCount > 0 && MinesCount < Width * Height;
|
||||
public bool IsValid => MinSizeX > 0 && MinSizeY > 0 && MaxSizeX >= MinSizeX && MaxSizeY >= MinSizeY && MaxSizeX * MaxSizeY > 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
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 IGameSettingsService settingsService;
|
||||
private readonly Random random = new Random();
|
||||
private CellData[,] cells;
|
||||
|
||||
public BoardService(MinesweeperGameConfig config)
|
||||
public BoardService(IGameSettingsService settingsService)
|
||||
{
|
||||
this.config = config;
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
public int Width { get; private set; }
|
||||
@@ -28,11 +23,9 @@ namespace Minesweeper.Core
|
||||
|
||||
public void InitializeEmptyBoard()
|
||||
{
|
||||
ResolveConfig(out var width, out var height, out var minesCount);
|
||||
|
||||
Width = width;
|
||||
Height = height;
|
||||
MinesCount = minesCount;
|
||||
Width = settingsService.SizeX;
|
||||
Height = settingsService.SizeY;
|
||||
MinesCount = Math.Min(settingsService.MinesCount, Width * Height - 1);
|
||||
OpenedSafeCellsCount = 0;
|
||||
IsGenerated = false;
|
||||
cells = new CellData[Width, Height];
|
||||
@@ -147,20 +140,6 @@ namespace Minesweeper.Core
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using Minesweeper.Config;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public sealed class GameSettingsService : IGameSettingsService
|
||||
{
|
||||
private const int MinimumMinesCount = 1;
|
||||
|
||||
private readonly MinesweeperGameConfig config;
|
||||
private readonly IGameSettingsStorage storage;
|
||||
private GameSettingsValue current;
|
||||
|
||||
public GameSettingsService(MinesweeperGameConfig config, IGameSettingsStorage storage)
|
||||
{
|
||||
this.config = config;
|
||||
this.storage = storage;
|
||||
current = LoadInitialSettings();
|
||||
}
|
||||
|
||||
public int SizeX => current.SizeX;
|
||||
public int SizeY => current.SizeY;
|
||||
public int MinesCount => current.MinesCount;
|
||||
public GameSettingsValue Current => current;
|
||||
|
||||
public GameSettingsValue Clamp(GameSettingsValue value)
|
||||
{
|
||||
var sizeX = Mathf.Clamp(value.SizeX, config.MinSizeX, config.MaxSizeX);
|
||||
var sizeY = Mathf.Clamp(value.SizeY, config.MinSizeY, config.MaxSizeY);
|
||||
EnsureAtLeastTwoCells(ref sizeX, ref sizeY);
|
||||
|
||||
var mines = Mathf.Clamp(value.MinesCount, MinimumMinesCount, GetMaxMines(sizeX, sizeY));
|
||||
return new GameSettingsValue(sizeX, sizeY, mines);
|
||||
}
|
||||
|
||||
public bool ApplyAndSaveIfChanged(GameSettingsValue value)
|
||||
{
|
||||
var clamped = Clamp(value);
|
||||
if (current.Equals(clamped))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = clamped;
|
||||
storage.Save(current);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetMaxMines(int sizeX, int sizeY)
|
||||
{
|
||||
return Mathf.Max(MinimumMinesCount, sizeX * sizeY - 1);
|
||||
}
|
||||
|
||||
private GameSettingsValue LoadInitialSettings()
|
||||
{
|
||||
if (storage.TryLoad(out var saved))
|
||||
{
|
||||
return Clamp(saved);
|
||||
}
|
||||
|
||||
return Clamp(new GameSettingsValue(config.MinSizeX, config.MinSizeY, MinimumMinesCount));
|
||||
}
|
||||
|
||||
private void EnsureAtLeastTwoCells(ref int sizeX, ref int sizeY)
|
||||
{
|
||||
if (sizeX * sizeY > 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sizeX < config.MaxSizeX)
|
||||
{
|
||||
sizeX++;
|
||||
}
|
||||
else if (sizeY < config.MaxSizeY)
|
||||
{
|
||||
sizeY++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38b7be3d055b5c34bbba9b36c0e11fc6
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public readonly struct GameSettingsValue
|
||||
{
|
||||
public GameSettingsValue(int sizeX, int sizeY, int minesCount)
|
||||
{
|
||||
SizeX = sizeX;
|
||||
SizeY = sizeY;
|
||||
MinesCount = minesCount;
|
||||
}
|
||||
|
||||
public int SizeX { get; }
|
||||
public int SizeY { get; }
|
||||
public int MinesCount { get; }
|
||||
|
||||
public bool Equals(GameSettingsValue other)
|
||||
{
|
||||
return SizeX == other.SizeX && SizeY == other.SizeY && MinesCount == other.MinesCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a48645a1753ceb4ba0401ec44d70070
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public interface IGameSettingsService
|
||||
{
|
||||
int SizeX { get; }
|
||||
int SizeY { get; }
|
||||
int MinesCount { get; }
|
||||
GameSettingsValue Current { get; }
|
||||
|
||||
GameSettingsValue Clamp(GameSettingsValue value);
|
||||
bool ApplyAndSaveIfChanged(GameSettingsValue value);
|
||||
int GetMaxMines(int sizeX, int sizeY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3dcff3a940f664b4b96722f28e46636d
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Minesweeper.Core
|
||||
{
|
||||
public interface IGameSettingsStorage
|
||||
{
|
||||
bool TryLoad(out GameSettingsValue value);
|
||||
void Save(GameSettingsValue value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54d04c5398018bc418e1a2528c559d0a
|
||||
@@ -28,6 +28,8 @@ namespace Minesweeper.Infrastructure
|
||||
builder.RegisterInstance(GetConfig());
|
||||
var resolvedUiConfig = GetUiConfig();
|
||||
builder.RegisterInstance(resolvedUiConfig);
|
||||
builder.Register<PlayerPrefsGameSettingsStorage>(Lifetime.Singleton).As<IGameSettingsStorage>();
|
||||
builder.Register<GameSettingsService>(Lifetime.Singleton).As<IGameSettingsService>();
|
||||
builder.Register<GameStateService>(Lifetime.Singleton).As<IGameStateService>();
|
||||
builder.Register<BoardService>(Lifetime.Singleton).As<IBoardService>();
|
||||
builder.Register<GamePauseService>(Lifetime.Singleton).As<IGamePauseService>();
|
||||
@@ -86,7 +88,6 @@ namespace Minesweeper.Infrastructure
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Minesweeper.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minesweeper.Infrastructure
|
||||
{
|
||||
public sealed class PlayerPrefsGameSettingsStorage : IGameSettingsStorage
|
||||
{
|
||||
private const string SizeXKey = "Minesweeper.Settings.SizeX";
|
||||
private const string SizeYKey = "Minesweeper.Settings.SizeY";
|
||||
private const string MinesCountKey = "Minesweeper.Settings.MinesCount";
|
||||
|
||||
public bool TryLoad(out GameSettingsValue value)
|
||||
{
|
||||
if (!PlayerPrefs.HasKey(SizeXKey) || !PlayerPrefs.HasKey(SizeYKey) || !PlayerPrefs.HasKey(MinesCountKey))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = new GameSettingsValue(PlayerPrefs.GetInt(SizeXKey), PlayerPrefs.GetInt(SizeYKey), PlayerPrefs.GetInt(MinesCountKey));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Save(GameSettingsValue value)
|
||||
{
|
||||
PlayerPrefs.SetInt(SizeXKey, value.SizeX);
|
||||
PlayerPrefs.SetInt(SizeYKey, value.SizeY);
|
||||
PlayerPrefs.SetInt(MinesCountKey, value.MinesCount);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a001fc2e924d95643a075190b6cadda1
|
||||
@@ -1,50 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c87c15d092dd420b85a09cc786496948
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -11,6 +11,7 @@ namespace Minesweeper.Presentation.Factories
|
||||
{
|
||||
private const string ContentImagePath = "Content/Image";
|
||||
private const string ContentLabelPath = "Content/Text (TMP)";
|
||||
private const string ContentPath = "Content";
|
||||
|
||||
private readonly MinesweeperUiConfig uiConfig;
|
||||
|
||||
@@ -33,9 +34,10 @@ namespace Minesweeper.Presentation.Factories
|
||||
|
||||
var button = go.GetComponent<Button>();
|
||||
var backgroundImage = go.GetComponent<Image>();
|
||||
var contentRoot = FindComponent<RectTransform>(go.transform, ContentPath);
|
||||
var contentImage = FindComponent<Image>(go.transform, ContentImagePath);
|
||||
var label = FindComponent<TMP_Text>(go.transform, ContentLabelPath);
|
||||
view.Bind(button, backgroundImage, contentImage, label);
|
||||
view.Bind(button, backgroundImage, contentRoot, contentImage, label);
|
||||
view.AutoBind();
|
||||
view.Initialize(cell.X, cell.Y);
|
||||
return view;
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace Minesweeper.Presentation.Factories
|
||||
private const string BoardGridName = "BoardGrid";
|
||||
private const string PausePanelName = "PausePanel";
|
||||
private const string ResultPanelName = "ResultPanel";
|
||||
private const string StartButtonPath = "StartButton";
|
||||
private const string RestartButtonPath = "RestartButton";
|
||||
private const string ContinueButtonPath = "ContinueButton";
|
||||
private const string MainMenuButtonPath = "MainMenuButton";
|
||||
@@ -40,7 +39,7 @@ namespace Minesweeper.Presentation.Factories
|
||||
var result = SpawnScreen(catalog.ResultPanelPrefab, contentRoot, ResultPanelName, 3);
|
||||
|
||||
var mainMenuView = RequireComponent<MainMenuView>(mainMenu.transform, MainMenuPanelName);
|
||||
mainMenuView.Bind(mainMenu, RequireChildComponent<Button>(mainMenu.transform, StartButtonPath));
|
||||
mainMenuView.BindRoot(mainMenu);
|
||||
|
||||
var refs = new MinesweeperScreenRefs(
|
||||
mainMenuView,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Minesweeper.Commands;
|
||||
using Minesweeper.Config;
|
||||
using Minesweeper.Core;
|
||||
using Minesweeper.Presentation.Views;
|
||||
|
||||
@@ -7,12 +8,16 @@ namespace Minesweeper.Presentation.Presenters
|
||||
public sealed class MainMenuPresenter : IPresenter
|
||||
{
|
||||
private readonly IGameCommandDispatcher commandDispatcher;
|
||||
private readonly MinesweeperGameConfig config;
|
||||
private readonly IGameSettingsService settingsService;
|
||||
private readonly IGameStateService gameStateService;
|
||||
private readonly IMainMenuView view;
|
||||
|
||||
public MainMenuPresenter(IGameCommandDispatcher commandDispatcher, IGameStateService gameStateService, IMainMenuView view = null)
|
||||
public MainMenuPresenter(IGameCommandDispatcher commandDispatcher, MinesweeperGameConfig config, IGameSettingsService settingsService, IGameStateService gameStateService, IMainMenuView view = null)
|
||||
{
|
||||
this.commandDispatcher = commandDispatcher;
|
||||
this.config = config;
|
||||
this.settingsService = settingsService;
|
||||
this.gameStateService = gameStateService;
|
||||
this.view = view;
|
||||
}
|
||||
@@ -22,7 +27,9 @@ namespace Minesweeper.Presentation.Presenters
|
||||
if (view != null)
|
||||
{
|
||||
view.StartClicked += OnStartClicked;
|
||||
view.SizeChanged += OnSizeChanged;
|
||||
gameStateService.StateChanged += OnStateChanged;
|
||||
RefreshMenuValues(settingsService.Current);
|
||||
OnStateChanged(gameStateService.Current);
|
||||
}
|
||||
}
|
||||
@@ -32,19 +39,29 @@ namespace Minesweeper.Presentation.Presenters
|
||||
if (view != null)
|
||||
{
|
||||
view.StartClicked -= OnStartClicked;
|
||||
view.SizeChanged -= OnSizeChanged;
|
||||
gameStateService.StateChanged -= OnStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartClicked()
|
||||
{
|
||||
settingsService.ApplyAndSaveIfChanged(view.SelectedSettings);
|
||||
commandDispatcher.Dispatch(new StartGameCommand());
|
||||
}
|
||||
|
||||
private void OnSizeChanged()
|
||||
{
|
||||
var selected = settingsService.Clamp(view.SelectedSettings);
|
||||
var maxMines = settingsService.GetMaxMines(selected.SizeX, selected.SizeY);
|
||||
view.ConfigureMines(1, maxMines, selected.MinesCount);
|
||||
}
|
||||
|
||||
private void OnStateChanged(GameState state)
|
||||
{
|
||||
if (state == GameState.FieldSelection)
|
||||
{
|
||||
RefreshMenuValues(settingsService.Current);
|
||||
view.Show();
|
||||
}
|
||||
else
|
||||
@@ -52,5 +69,13 @@ namespace Minesweeper.Presentation.Presenters
|
||||
view.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshMenuValues(GameSettingsValue settings)
|
||||
{
|
||||
var clamped = settingsService.Clamp(settings);
|
||||
view.ConfigureSizeX(config.MinSizeX, config.MaxSizeX, clamped.SizeX);
|
||||
view.ConfigureSizeY(config.MinSizeY, config.MaxSizeY, clamped.SizeY);
|
||||
view.ConfigureMines(1, settingsService.GetMaxMines(clamped.SizeX, clamped.SizeY), clamped.MinesCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
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 IGameSettingsService settingsService;
|
||||
private readonly IGameStateService gameStateService;
|
||||
|
||||
public GameReadModel(MinesweeperGameConfig config, IBoardService boardService, IGameStateService gameStateService)
|
||||
public GameReadModel(IBoardService boardService, IGameSettingsService settingsService, IGameStateService gameStateService)
|
||||
{
|
||||
this.config = config;
|
||||
this.boardService = boardService;
|
||||
this.settingsService = settingsService;
|
||||
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 Width => boardService.Width > 0 ? boardService.Width : settingsService.SizeX;
|
||||
public int Height => boardService.Height > 0 ? boardService.Height : settingsService.SizeY;
|
||||
public int MinesCount => boardService.MinesCount > 0 ? boardService.MinesCount : settingsService.MinesCount;
|
||||
public int FlaggedCellsCount => CountFlaggedCells();
|
||||
public int RemainingMinesCount => MinesCount - FlaggedCellsCount;
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
private const string ContentImagePath = "Content/Image";
|
||||
private const string ContentLabelPath = "Content/Text (TMP)";
|
||||
private const string ContentPath = "Content";
|
||||
|
||||
[SerializeField] private Button button;
|
||||
[SerializeField] private Image backgroundImage;
|
||||
[SerializeField] private RectTransform contentRoot;
|
||||
[SerializeField] private Image contentImage;
|
||||
[SerializeField] private TMP_Text label;
|
||||
|
||||
@@ -28,10 +30,11 @@ namespace Minesweeper.Presentation.Views
|
||||
public event Action PressStarted;
|
||||
public event Action PressEnded;
|
||||
|
||||
public void Bind(Button button, Image backgroundImage, Image contentImage, TMP_Text label)
|
||||
public void Bind(Button button, Image backgroundImage, RectTransform contentRoot, Image contentImage, TMP_Text label)
|
||||
{
|
||||
this.button = button;
|
||||
this.backgroundImage = backgroundImage;
|
||||
this.contentRoot = contentRoot;
|
||||
this.contentImage = contentImage;
|
||||
this.label = label;
|
||||
}
|
||||
@@ -48,6 +51,15 @@ namespace Minesweeper.Presentation.Views
|
||||
backgroundImage = GetComponent<Image>();
|
||||
}
|
||||
|
||||
if (contentRoot == null)
|
||||
{
|
||||
var contentTransform = transform.Find(ContentPath);
|
||||
if (contentTransform != null)
|
||||
{
|
||||
contentRoot = contentTransform.GetComponent<RectTransform>();
|
||||
}
|
||||
}
|
||||
|
||||
if (contentImage == null)
|
||||
{
|
||||
var contentImageTransform = transform.Find(ContentImagePath);
|
||||
@@ -82,12 +94,18 @@ namespace Minesweeper.Presentation.Views
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(BoardCellData cell, MinesweeperUiConfig config, float pixelsPerUnitMultiplier, bool revealUnflaggedMines)
|
||||
public void Render(BoardCellData cell, MinesweeperUiConfig config, float pixelsPerUnitMultiplier, float contentPadding, bool revealUnflaggedMines)
|
||||
{
|
||||
gameObject.name = $"bt_{cell.X}_{cell.Y}_{cell.DisplayValue}";
|
||||
isOpened = cell.IsOpened;
|
||||
var revealMine = revealUnflaggedMines && cell.IsMine && !cell.IsFlagged;
|
||||
|
||||
if (contentRoot != null)
|
||||
{
|
||||
contentRoot.offsetMin = new Vector2(contentPadding, contentPadding);
|
||||
contentRoot.offsetMax = new Vector2(-contentPadding, -contentPadding);
|
||||
}
|
||||
|
||||
if (backgroundImage != null)
|
||||
{
|
||||
backgroundImage.pixelsPerUnitMultiplier = pixelsPerUnitMultiplier;
|
||||
|
||||
@@ -13,6 +13,10 @@ namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
private const float ResizeRefreshDelaySeconds = 0.5f;
|
||||
private const float ResizeSizeEpsilon = 0.5f;
|
||||
private const float MaximumCellPixelsPerUnitMultiplier = 1f;
|
||||
private const float ContentPaddingReferenceCellSize = 202f;
|
||||
private const float ContentPaddingReferencePadding = 15f;
|
||||
private const float MinimumContentPadding = 1f;
|
||||
|
||||
[SerializeField] private GameObject gameRoot;
|
||||
[SerializeField] private GameObject pauseRoot;
|
||||
@@ -37,6 +41,7 @@ namespace Minesweeper.Presentation.Views
|
||||
private bool resizeRefreshPending;
|
||||
private int currentBoardWidth;
|
||||
private int currentBoardHeight;
|
||||
private float currentContentPadding = MinimumContentPadding;
|
||||
private float currentPixelsPerUnitMultiplier = 1f;
|
||||
private float resizeStableAt;
|
||||
private Vector2 lastObservedLayoutSize;
|
||||
@@ -195,7 +200,7 @@ namespace Minesweeper.Presentation.Views
|
||||
var cell = cells[i];
|
||||
if (cellsByCoordinate.TryGetValue(ToKey(cell.X, cell.Y), out var view))
|
||||
{
|
||||
view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, revealUnflaggedMines);
|
||||
view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, currentContentPadding, revealUnflaggedMines);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,10 +353,16 @@ namespace Minesweeper.Presentation.Views
|
||||
gridLayoutGroup.padding = new RectOffset();
|
||||
gridLayoutGroup.spacing = new Vector2(spacing, spacing);
|
||||
gridLayoutGroup.cellSize = new Vector2(cellSize, cellSize);
|
||||
currentPixelsPerUnitMultiplier = uiConfig.ReferenceCellSize / cellSize;
|
||||
currentPixelsPerUnitMultiplier = Mathf.Min(MaximumCellPixelsPerUnitMultiplier, uiConfig.ReferenceCellSize / cellSize);
|
||||
currentContentPadding = CalculateContentPadding(cellSize);
|
||||
lastObservedLayoutSize = layoutSize;
|
||||
}
|
||||
|
||||
private static float CalculateContentPadding(float cellSize)
|
||||
{
|
||||
return Mathf.Max(MinimumContentPadding, cellSize * ContentPaddingReferencePadding / ContentPaddingReferenceCellSize);
|
||||
}
|
||||
|
||||
private Vector2 GetLayoutSourceSize()
|
||||
{
|
||||
var parentRect = boardPanel.parent as RectTransform;
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
using System;
|
||||
using Minesweeper.Core;
|
||||
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
public interface IMainMenuView : IView
|
||||
{
|
||||
event Action StartClicked;
|
||||
event Action SizeChanged;
|
||||
|
||||
GameSettingsValue SelectedSettings { get; }
|
||||
|
||||
void Show();
|
||||
void Hide();
|
||||
void ConfigureSizeX(int min, int max, int value);
|
||||
void ConfigureSizeY(int min, int max, int value);
|
||||
void ConfigureMines(int min, int max, int value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Minesweeper.Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
@@ -8,8 +9,14 @@ namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
[SerializeField] private GameObject root;
|
||||
[SerializeField] private Button startButton;
|
||||
[SerializeField] private MenuSliderView sizeXSlider = new MenuSliderView();
|
||||
[SerializeField] private MenuSliderView sizeYSlider = new MenuSliderView();
|
||||
[SerializeField] private MenuSliderView minesSlider = new MenuSliderView();
|
||||
|
||||
public event Action StartClicked;
|
||||
public event Action SizeChanged;
|
||||
|
||||
public GameSettingsValue SelectedSettings => new GameSettingsValue(sizeXSlider.Value, sizeYSlider.Value, minesSlider.Value);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -17,8 +24,6 @@ namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
root = gameObject;
|
||||
}
|
||||
|
||||
AutoBind();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -27,6 +32,12 @@ namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
startButton.onClick.AddListener(OnStartClicked);
|
||||
}
|
||||
|
||||
sizeXSlider.ValueChanged += OnSizeSliderChanged;
|
||||
sizeYSlider.ValueChanged += OnSizeSliderChanged;
|
||||
sizeXSlider.AddListeners();
|
||||
sizeYSlider.AddListeners();
|
||||
minesSlider.AddListeners();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
@@ -35,6 +46,12 @@ namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
startButton.onClick.RemoveListener(OnStartClicked);
|
||||
}
|
||||
|
||||
sizeXSlider.RemoveListeners();
|
||||
sizeYSlider.RemoveListeners();
|
||||
minesSlider.RemoveListeners();
|
||||
sizeXSlider.ValueChanged -= OnSizeSliderChanged;
|
||||
sizeYSlider.ValueChanged -= OnSizeSliderChanged;
|
||||
}
|
||||
|
||||
public void Show()
|
||||
@@ -47,27 +64,34 @@ namespace Minesweeper.Presentation.Views
|
||||
root.SetActive(false);
|
||||
}
|
||||
|
||||
public void ConfigureSizeX(int min, int max, int value)
|
||||
{
|
||||
sizeXSlider.Configure(min, max, value, "Size X");
|
||||
}
|
||||
|
||||
public void ConfigureSizeY(int min, int max, int value)
|
||||
{
|
||||
sizeYSlider.Configure(min, max, value, "Size Y");
|
||||
}
|
||||
|
||||
public void ConfigureMines(int min, int max, int value)
|
||||
{
|
||||
minesSlider.Configure(min, max, value, "Mines Count");
|
||||
}
|
||||
|
||||
private void OnStartClicked()
|
||||
{
|
||||
StartClicked?.Invoke();
|
||||
}
|
||||
|
||||
public void Bind(GameObject root, Button startButton)
|
||||
public void BindRoot(GameObject root)
|
||||
{
|
||||
this.root = root != null ? root : gameObject;
|
||||
this.startButton = startButton;
|
||||
}
|
||||
|
||||
private void AutoBind()
|
||||
private void OnSizeSliderChanged(int value)
|
||||
{
|
||||
if (startButton == null)
|
||||
{
|
||||
var startButtonTransform = transform.Find("StartButton");
|
||||
if (startButtonTransform != null)
|
||||
{
|
||||
startButton = startButtonTransform.GetComponent<Button>();
|
||||
}
|
||||
}
|
||||
SizeChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class MenuSliderView
|
||||
{
|
||||
[SerializeField] private Slider slider;
|
||||
[SerializeField] private TMP_Text minText;
|
||||
[SerializeField] private TMP_Text maxText;
|
||||
[SerializeField] private TMP_Text valueText;
|
||||
[SerializeField] private string valueLabel;
|
||||
|
||||
public event Action<int> ValueChanged;
|
||||
|
||||
public int Value => slider != null ? Mathf.RoundToInt(slider.value) : 0;
|
||||
|
||||
public void Bind(Slider slider, TMP_Text minText, TMP_Text maxText, TMP_Text valueText)
|
||||
{
|
||||
RemoveListeners();
|
||||
this.slider = slider;
|
||||
this.minText = minText;
|
||||
this.maxText = maxText;
|
||||
this.valueText = valueText;
|
||||
AddListeners();
|
||||
}
|
||||
|
||||
public void AddListeners()
|
||||
{
|
||||
if (slider != null)
|
||||
{
|
||||
slider.onValueChanged.AddListener(OnValueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveListeners()
|
||||
{
|
||||
if (slider != null)
|
||||
{
|
||||
slider.onValueChanged.RemoveListener(OnValueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(int min, int max, int value, string label)
|
||||
{
|
||||
if (slider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
valueLabel = label;
|
||||
var clampedMax = Mathf.Max(min, max);
|
||||
var clampedValue = Mathf.Clamp(value, min, clampedMax);
|
||||
slider.wholeNumbers = true;
|
||||
slider.minValue = min;
|
||||
slider.maxValue = clampedMax;
|
||||
slider.SetValueWithoutNotify(clampedValue);
|
||||
SetText(minText, min);
|
||||
SetText(maxText, clampedMax);
|
||||
SetValueText(clampedValue);
|
||||
}
|
||||
|
||||
private void OnValueChanged(float value)
|
||||
{
|
||||
var intValue = Mathf.RoundToInt(value);
|
||||
SetValueText(intValue);
|
||||
ValueChanged?.Invoke(intValue);
|
||||
}
|
||||
|
||||
private void SetValueText(int value)
|
||||
{
|
||||
if (valueText != null)
|
||||
{
|
||||
valueText.text = string.IsNullOrEmpty(valueLabel) ? value.ToString() : $"{valueLabel}: {value}";
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetText(TMP_Text text, int value)
|
||||
{
|
||||
if (text != null)
|
||||
{
|
||||
text.text = value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15be53f58e067f944a33854111083046
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Minesweeper.Core;
|
||||
|
||||
namespace Minesweeper.Presentation.Views
|
||||
{
|
||||
@@ -10,6 +11,14 @@ namespace Minesweeper.Presentation.Views
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action SizeChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public GameSettingsValue SelectedSettings => default;
|
||||
|
||||
public void Show()
|
||||
{
|
||||
}
|
||||
@@ -17,5 +26,17 @@ namespace Minesweeper.Presentation.Views
|
||||
public void Hide()
|
||||
{
|
||||
}
|
||||
|
||||
public void ConfigureSizeX(int min, int max, int value)
|
||||
{
|
||||
}
|
||||
|
||||
public void ConfigureSizeY(int min, int max, int value)
|
||||
{
|
||||
}
|
||||
|
||||
public void ConfigureMines(int min, int max, int value)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user