/// <summary>
        /// constructor. this builds a fresh map using the same dimensions as given by the parameters and populates it with the
        /// same number of mines given. if the number of mines can't be less than 1, and there has to be at least one non-mine cell.
        /// </summary>
        /// <param name="width">how wide each row in the map should be</param>
        /// <param name="height">how tall each column in the map should be</param>
        /// <param name="mines">how many mines should be in this map. this constructor ensures there is at least
        /// one mine and one non-mine cell in the map.</param>
        public MinesweeperMap(int width, int height, int mines)
        {
            numCells      = width * height;
            rowLength     = width;
            flaggedCells  = 0;
            revealedCells = 0;
            cells         = new MinesweeperCell[numCells];

            // populate the new map with cells
            for (int idx = 0; idx < numCells; idx++)
            {
                cells[idx] = new MinesweeperCell();
            }

            // if the desired number of mines is too great or too little, the real number of mines needs to be capped.
            // there must be at least one mine and one non-mine cell.
            if (mines < 1)
            {
                numMines = 1;
            }
            else if (mines > numCells - 1)
            {
                numMines = numCells - 1;
            }
            else
            {
                numMines = mines;
            }
        }
        /// <summary>
        /// reveals a given cell if it hasn't been revealed already. if it has been flagged or already revealed, nothing happens.
        /// throws GameOverException if the revealed cell is a mine or if the game has been won, with a message of either "won" or "lost".
        /// </summary>
        /// <param name="x">the x coordinate of the targeted cell.</param>
        /// <param name="y">the y coordinate of the targeted cell.</param>
        public void RevealCell(int x, int y)
        {
            MinesweeperCell cell = cells[x + (y * rowLength)];

            // flagged cells cannot be revealed, need to check if the cell is flagged first and do nothing else if it is.
            // revealed cells don't need to be revealed again either, so we skip those too.
            if (!cell.isFlagged && !cell.isVisible)
            {
                cell.isVisible = true;
                revealedCells++;

                // if the cell is a mine, the player has lost.
                if (cell.hasMine)
                {
                    throw new GameOverException("lost");
                }

                // if the cell was the last non-mine cell, the player has won.
                if (revealedCells == numCells - numMines)
                {
                    throw new GameOverException("won");
                }

                // if this cell has no neighboring mines, we can safely reveal all neighboring cells for the player automatically.
                else if (cell.neighboringMines == 0)
                {
                    RevealNeighbors(x, y);
                }
            }
        }
        /// <summary>
        /// private helper method. this makes the targeted cell a mine and increments the mine counts of all its neighbors.
        /// does nothing if the cell already has a mine.
        /// </summary>
        /// <param name="x">the x coordinate of the targeted cell.</param>
        /// <param name="y">the y coordinate of the targeted cell.</param>
        private void SetMine(int x, int y)
        {
            MinesweeperCell target = cells[x + (y * rowLength)];

            // only proceed if this cell doesn't have a mine already to avoid incorrectly incrementing neighbors
            if (!target.hasMine)
            {
                target.hasMine = true;
                foreach ((int x, int y)position in GetCellNeighborPositions(x, y))
                {
                    GetCell(position.x, position.y).neighboringMines++;
                }
            }
        }
        /// <summary>
        /// flags a given cell if it isn't flagged, or unflags a cell if it is flagged.
        /// </summary>
        /// <param name="x">the x coordinate of the targeted cell.</param>
        /// <param name="y">the y coordinate of the targeted cell.</param>
        public void MarkCell(int x, int y)
        {
            MinesweeperCell target = cells[x + (y * rowLength)];

            if (target.isFlagged)
            {
                target.isFlagged = false;
                flaggedCells--;
            }
            else
            {
                target.isFlagged = true;
                flaggedCells++;
            }
        }