using System; using UnityEngine; using Random = UnityEngine.Random; using System.Collections.Generic; using System.Linq; using UnityEngine.UI; #region Structs/Enums /// /// States of the game /// public enum EGameState { Uninitialized, Playing, Win, Lose } #endregion /// /// The control script of the game, defines the logic. Should have been a singleton. /// public class ControlScript : MonoBehaviour { #region Fields /// /// Width of the next generated grid /// [Header("Field properties")] public int GridSizeX; /// /// Height of the next generated grid /// public int GridSizeY; /// /// Number of mines for the next game /// [Range(0, 999)] public int TotalMines; /// /// Bounds so the field does not get too small or too big /// public float MinScale, MaxScale; /// /// Reference to the scrolling (game view) camera /// [Header("Object references")] public GameObject ScrollingCamera; /// /// Reference to the screen overlay canvas (doesn't do anything) /// public GameObject OverlayCanvas; /// /// Reference to the button that shows the current state of the game (playing or dead) /// public Image StateButton; /// /// Reference to all bomb number objects /// public BombNumberScript[] BombNumbers; /// /// Reference to all time number objects /// public TimeScript[] TimeNumbers; /// /// Prefab of a single tile /// public GameObject TilePrefab; /// /// Prefab of some information /// public GameObject PersistentInfoPrefab; /// /// Prefab for the options form when the user clicks on the cog /// public GameObject OptionsFormPrefab; // Private vars private PersistentInfoScript _persistentInfo; private GameObject OptionsForm; private GameObject[,] _tiles; private int _numRevealedTiles; private int _numBombsLeft; private EGameState _gameState = EGameState.Uninitialized; private float _startTime; private TimeSpan _showTime; private bool _updateTime; public GameObject ParentCanvas; public AudioClip loseSound; public AudioClip winSound; public float timeLimitSeconds = 10f; #endregion #region Properties /// /// Current state of the game. When set, also changes the actual state /// public EGameState GameState { set { _gameState = value; OnGameStateChange(); } get { return _gameState; } } /// /// Number of bombs that are left on the field /// public int NumBombsLeft { get { return _numBombsLeft; } set { _numBombsLeft = value; } } /// /// Rectangle that gives the current visible part of the field /// public Rect VisibleField { get { var cam = ScrollingCamera.GetComponent(); var bottomLeft = cam.ViewportToWorldPoint(new Vector3(0, 0, cam.nearClipPlane)); var topRight = cam.ViewportToWorldPoint(new Vector3(1, 1, cam.nearClipPlane)); return new Rect(bottomLeft, topRight - bottomLeft); } } /// /// The controls /// public ActionMap Controls { get { return _persistentInfo.Controls; } } #endregion #region Unity methods /// /// Use this for initialization /// void Start() { // Check if PersistentInfo exists var info = GameObject.FindGameObjectsWithTag("PersistentInfo"); switch (info.Length) { case 0: _persistentInfo = Instantiate(PersistentInfoPrefab).GetComponent(); break; case 1: _persistentInfo = info[0].GetComponent(); CopyPersistentInfo(); break; default: //TODO: fix it throw new MissingComponentException("Multiple copies of PersistentInfo were found"); } // Assign some variables BombNumbers[0].Parent = this; BombNumbers[1].Parent = this; BombNumbers[2].Parent = this; // set layer BombNumbers[0].GetComponent().sortingOrder = 5; BombNumbers[1].GetComponent().sortingOrder = 5; BombNumbers[2].GetComponent().sortingOrder = 5; // Set camera bounds // ScrollingCamera.GetComponent().Reset(); CreateTiles(); } /// /// Update is called once per frame /// void Update() { // Update timer if (_updateTime) { _showTime = TimeSpan.FromSeconds(Time.time - _startTime); Array.ForEach(TimeNumbers, tnum => tnum.SetTimeSprite(_showTime)); // if above the time limit, lose if (_showTime.TotalSeconds > timeLimitSeconds) { GameState = EGameState.Lose; CheckWin(); } } // check if user is pressing the M key and auto win if (Input.GetKeyDown(KeyCode.M)) { GameState = EGameState.Win; CheckWin(); } } #endregion #region Methods /// /// Places mines randomly on the field /// public void PlaceMines() { PlaceMines(-1, -1); } /// /// Places mines on the field and make sure that the given position is an empty tile /// /// /// public void PlaceMines(int posX, int posY) { // Create list of all possible mine locations HashSet tileList = new HashSet(new Vector2iComparer()); for (int i = 0; i < GridSizeX; i++) { for (int j = 0; j < GridSizeY; j++) { tileList.Add(new Vector2i(i, j)); } } // Create safe zone if specified if (posX >= 0 && posY >= 0) { for (int i = posX - 1; i <= posX + 1; i++) { for (int j = posY - 1; j <= posY + 1; j++) { tileList.Remove(new Vector2i(i, j)); } } } // Place rest of mines in field int minesPlaced = 0; while (minesPlaced < TotalMines && tileList.Count > 0) { var r = tileList.ElementAt(Random.Range(0, tileList.Count)); var tile = _tiles[r.X, r.Y].GetComponent(); tile.Type = TileScript.EType.Mine; // Set numbers around it accordingly foreach (var neighbour in tile.Neighbours) { neighbour.Number++; } // Remove placed mine from list tileList.Remove(r); minesPlaced++; } // Update number of mines and gamestate TotalMines = minesPlaced; _numBombsLeft = minesPlaced; UpdateBombNumber(); GameState = EGameState.Playing; StartTimer(); _updateTime = true; } /// /// Check if field is initialized, place mines if not /// /// /// public void CheckInitialization(int posX, int posY) { if (_gameState == EGameState.Uninitialized) PlaceMines(posX, posY); } /// /// Check for win conditions /// /// public EGameState CheckWin() { if (_numRevealedTiles >= GridSizeX * GridSizeY - TotalMines && _gameState != EGameState.Lose) { SoundFXManager.instance.PlaySound(winSound, transform, 1.0f); return EGameState.Win; } if (_gameState == EGameState.Lose) { SoundFXManager.instance.PlaySound(loseSound, transform, 1.0f); // restart with dealy of 1 second Invoke("Restart", 1.0f); } return _gameState; } /// /// Set state as losing (reveals all mines) /// public void SetLoseState() { _gameState = EGameState.Lose; RevealAllMines(); } /// /// Restart the game /// public void Restart() { foreach (var tile in _tiles) { Destroy(tile); } _gameState = EGameState.Uninitialized; _numRevealedTiles = 0; CreateTiles(); ResetTimer(); StateButton.sprite = StateButton.GetComponent().DefaultSprite; } #endregion #region Helper Methods /// /// Copy all info from PersistentInfo /// private void CopyPersistentInfo() { GridSizeX = _persistentInfo.DefaultFieldSizeX; GridSizeY = _persistentInfo.DefaultFieldSizeY; TotalMines = _persistentInfo.DefaultMines; } /// /// Removes any interaction with all tiles /// private void DisablePlayField() { StopTimer(); foreach (var tile in _tiles) { tile.GetComponent().enabled = false; tile.GetComponent().Enabled = false; } } /// /// Re-enables all interaction with tiles /// private void EnablePlayField() { foreach (var tile in _tiles) { tile.GetComponent().Enabled = true; } if (_gameState == EGameState.Playing) Debug.Log("Resuming timer"); ResumeTimer(); } /// /// Initializes and places them centralized around (0,0), /// ensuring they are parented to the UI canvas and have correct rendering layers. /// private void CreateTiles() { // Reset scrolling camera var camScript = ScrollingCamera.GetComponent(); camScript.Reset(); // Find scale to draw tiles with var tileRenderSize = TilePrefab.GetComponent().bounds.size; // Debug.Log("Tile render size: " + tileRenderSize); var fieldRenderSize = new Vector3(GridSizeX * tileRenderSize.x, GridSizeY * tileRenderSize.y); var cameraWorldViewSize = VisibleField; // get scale to draw tiles with var scale = Mathf.Clamp(Math.Min(cameraWorldViewSize.width / fieldRenderSize.x, cameraWorldViewSize.height / fieldRenderSize.y), MinScale, MaxScale); // Debug.Log("Calculation: " + cameraWorldViewSize.width + " / " + fieldRenderSize.x + " = " + cameraWorldViewSize.width / fieldRenderSize.x); // Debug.Log("Calculation: " + cameraWorldViewSize.height + " / " + fieldRenderSize.y + " = " + cameraWorldViewSize.height / fieldRenderSize.y); // Debug.Log("Scale: " + scale); scale = 300; tileRenderSize *= scale; fieldRenderSize *= scale; // Debug.Log("Tile render size: " + tileRenderSize); // Debug.Log("Field render size: " + fieldRenderSize); // Update scrolling camera camScript.MaxUpperView = 0f; camScript.MaxLeftView = Math.Min(-fieldRenderSize.x / 2f, cameraWorldViewSize.xMin); camScript.MaxRightView = Math.Max(fieldRenderSize.x / 2f, cameraWorldViewSize.xMax); camScript.MaxLowerView = Math.Min(-fieldRenderSize.y, cameraWorldViewSize.yMin); // Create new container _tiles = new GameObject[GridSizeX, GridSizeY]; for (int i = 0; i < GridSizeX; i++) { for (int j = 0; j < GridSizeY; j++) { // Instantiate tiles under the Canvas _tiles[i, j] = Instantiate(TilePrefab, ParentCanvas.transform); // Set position _tiles[i, j].transform.localPosition = new Vector3((i + .5f) * tileRenderSize.x - fieldRenderSize.x / 2f, -j * tileRenderSize.y); _tiles[i, j].transform.localScale = Vector3.one; // Ensure consistent scaling // Debug.Log("Creating tile at: " + _tiles[i, j].transform.localPosition); // Assign layers _tiles[i, j].layer = 5; // UI layer var tileRenderer = _tiles[i, j].GetComponent(); if (tileRenderer != null) { tileRenderer.renderingLayerMask = 1 << 2; // Light Layer 2 } // Initialize tile script var tile = _tiles[i, j].GetComponent(); tile.Parent = this; tile.Container = _tiles; tile.GridPos = new Vector2i(i, j); tile.LocalScale = scale; // Debug.Log("Edit tile at: " + _tiles[i, j].transform.localPosition); } } // Assign neighbours for (int i = 0; i < GridSizeX; i++) { for (int j = 0; j < GridSizeY; j++) { for (int ii = i - 1; ii <= i + 1; ii++) { for (int jj = j - 1; jj <= j + 1; jj++) { if (i == ii && j == jj) continue; try { _tiles[i, j].GetComponent().NewNeighbour = _tiles[ii, jj].GetComponent(); } catch (IndexOutOfRangeException) { } //ignore } } } } // Set number of mines back to original value _numBombsLeft = TotalMines; UpdateBombNumber(); } /// /// Starts the timer /// private void StartTimer() { _startTime = Time.time; Debug.Log("Start time: " + _startTime); ResetTimer(); } /// /// Resumes the timer from a pauzed state /// private void ResumeTimer() { _startTime += (float)(Time.time - (_startTime + _showTime.TotalSeconds)); _updateTime = true; } /// /// Stops the timer /// private void StopTimer() { _showTime = TimeSpan.FromSeconds(Time.time - _startTime); _updateTime = false; } /// /// Sets the timer to 0 without starting it /// private void ResetTimer() { _showTime = new TimeSpan(); _updateTime = false; } /// /// Reveals all the mines /// private void RevealAllMines() { foreach (var tile in _tiles) { tile.GetComponent().RevealAsLoseState(); } } /// /// Updates the bomb number /// private void UpdateBombNumber() { Array.ForEach(BombNumbers, bnum => bnum.SetNumber(_numBombsLeft)); Debug.Log("Number of bombs left: " + _numBombsLeft); } #endregion #region Events /// /// Is called whenever GameState is altered /// private void OnGameStateChange() { switch (_gameState) { case EGameState.Lose: // Actions to do when lost //StateButton.LoseState(); StateButton.sprite = StateButton.GetComponent().LoseSprite; RevealAllMines(); DisablePlayField(); break; case EGameState.Win: // Actions to do when won //StateButton.WinState(); StateButton.sprite = StateButton.GetComponent().WinSprite; DisablePlayField(); break; } } /// /// What to do when a tile has been clicked /// /// public void OnTileClick(GameObject sender) { var tile = sender.GetComponent(); // Place mines if field is uninitialized if (_gameState == EGameState.Uninitialized) PlaceMines(tile.GridPos.X, tile.GridPos.Y); // Don't do anything unless the game is playing if (_gameState != EGameState.Playing) return; tile.Revealed = true; if (tile.Revealed && tile.Type == TileScript.EType.Mine) { Debug.Log("You clicked on a mine!"); GameState = EGameState.Lose; sender.GetComponent().SetAsDeadMine(); } GameState = CheckWin(); } /// /// What to do when a tile was revealed /// /// public void OnTileReveal(GameObject sender) { _numRevealedTiles++; } /// /// What to do when a tile has been right clicked /// /// public void OnTileRightClick(GameObject sender) { var tile = sender.GetComponent(); _numBombsLeft += tile.Flagged.ToIntSign(); tile.Flagged = !tile.Flagged; UpdateBombNumber(); } /// /// What to do when a tile has been middle clicked /// /// public void OnTileMiddleClick(GameObject sender) { var tile = sender.GetComponent(); if ((int)tile.Number == tile.Neighbours.Count(neighbour => neighbour.Flagged)) tile.Neighbours.ForEach(neighbour => neighbour.Revealed = true); GameState = CheckWin(); } /// /// What to do if a bomb number was clicked /// /// public void OnBombNumberClick(BombNumberScript.EDigit digit) { switch (digit) { case BombNumberScript.EDigit.First: TotalMines += (TotalMines % 10 != 9 ? 1 : -9); break; case BombNumberScript.EDigit.Second: TotalMines += ((TotalMines / 10) % 10 != 9 ? 10 : -90); break; case BombNumberScript.EDigit.Third: TotalMines += (TotalMines / 100 != 9 ? 100 : -900); break; default: throw new ArgumentOutOfRangeException("digit"); } _numBombsLeft = TotalMines; UpdateBombNumber(); } /// /// What to do if a bomb number was right clicked /// /// public void OnBombNumberRightClick(BombNumberScript.EDigit digit) { switch (digit) { case BombNumberScript.EDigit.First: TotalMines -= (TotalMines % 10 != 0 ? 1 : -9); break; case BombNumberScript.EDigit.Second: TotalMines -= ((TotalMines / 10) % 10 != 0 ? 10 : -90); break; case BombNumberScript.EDigit.Third: TotalMines -= (TotalMines / 100 != 0 ? 100 : -900); break; default: throw new ArgumentOutOfRangeException("digit"); } _numBombsLeft = TotalMines; UpdateBombNumber(); } /// /// What to do when the options button is clicked /// public void OnOptionsButtonClick() { // Check if an options form is already present if (OptionsForm != null) { OnOptionsButtonCancelClick(); return; } // Disable playing field DisablePlayField(); OptionsForm = Instantiate(OptionsFormPrefab); OptionsForm.transform.SetParent(OverlayCanvas.transform, false); var buttons = OptionsForm.GetComponentsInChildren