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() {
if (_tiles == null)
return;
if (_tiles.Length == 0)
return;
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
Debug.Log("You 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