2
\$\begingroup\$

A Minesweeper library that can be included with any GUI.

It is an update to Minesweeper library for any GUI. I have done many updates with regards to the answer.

Three sections: Game, Board and Tile.
Then a unit test class: GameTest

Game

public enum GameState
{
    NotStarted,
    Started,
    Lost,
    Won
}

public interface IGame
{
    event Action<GameState> OnStateChanged;
    GameState State { get; }
    int Moves { get; }
    IBoard Board { get; }

    void NewGame(int columns, int rows, int mines);
    bool Reveal(int x, int y);
    IEnumerable<Tile> Revealed { get; }

    Tile Flag(int x, int y);
    Tile Unflag(int x, int y);
}

public class Game : IGame
{
    public event Action<GameState> OnStateChanged;

    private GameState _state = GameState.NotStarted;

    /// <summary>
    /// Current state of the game.
    /// </summary>
    public GameState State
    {
        get
        {
            return _state;
        }
        private set
        {
            _state = value;
            OnStateChanged?.Invoke(value);
        }
    }

    public IBoard Board { get; private set; }
    public int Moves { get; private set; }

    private int _tiles;
    private int _mines;
    private int _safe;

    /// <summary>
    /// Start a new game.
    /// </summary>
    public void NewGame(int columns, int rows, int mines)
    {
        if (columns < 1 || rows < 1)
            throw new ArgumentException("Board can't be smaller than 1x1.");

        if (mines >= columns * rows)
            throw new ArgumentException("Must be fewer mines than tiles.");

        State = GameState.Started;
        Board = new Board(columns, rows);
        Moves = 0;
        PlaceMines(mines);

        _tiles = columns * rows;
        _mines = mines;
        _safe = _tiles - _mines;
    }

    /// <summary>
    /// Place mines on random locations the board.
    /// </summary>
    private void PlaceMines(int mines)
    {
        var random = new Random();
        var placed = 0;

        while (placed < mines)
        {
            var x = random.Next(Board.Columns);
            var y = random.Next(Board.Rows);

            if (!Board.Tiles[x, y].HasMine)
            {
                Board.Tiles[x, y].HasMine = true;
                placed++;
            }
        }

        SetAdjacentMinesCount();
    }

    /// <summary>
    /// Relocate a mine to a random location.
    /// </summary>
    private void RelocateMine(int fromX, int fromY)
    {
        if (!Board.Tiles[fromX, fromY].HasMine)
            throw new InvalidOperationException("There is no mine on the given location.");

        var random = new Random();
        int toX, toY;

        do
        {
            toX = random.Next(Board.Columns);
            toY = random.Next(Board.Rows);
        } while ((toX == fromX && toY == fromY) || Board.Tiles[toX, toY].HasMine || Board.Tiles[toX, toY].IsRevealed());

        Board.Tiles[fromX, fromY].HasMine = false;
        Board.Tiles[toX, toY].HasMine = true;

        SetAdjacentMinesCount();
    }

    /// <summary>
    /// Set adjacent mines count for all tiles.
    /// </summary>
    private void SetAdjacentMinesCount()
    {
        foreach (var tile in Board.Tiles.Cast<Tile>())
        {
            Board.ForEachAdjacentTo(tile, (a) =>
            {
                if (a.HasMine)
                    tile.AdjacentMines++;
            });
        }
    }

    /// <summary>
    /// Reveal tiles starting from the initial tile.
    /// Returns true if safe, false if mine.
    /// If a mine is revealed, all mines become revealed.
    /// </summary>
    public bool Reveal(int initialX, int initialY)
    {
        if (State != GameState.Started)
            throw new InvalidOperationException("There is no game in progress.");

        if (initialX >= Board.Columns || initialX < 0 || initialY >= Board.Rows || initialY < 0)
            throw new ArgumentOutOfRangeException("Given (x, y) out of bounds.");

        // Get the initial tile instance.

        var initialTile = Board.Tiles[initialX, initialY];

        // Check if it's already revealed.

        if (initialTile.IsRevealed())
            throw new InvalidOperationException("The given tile has already been revealed.");

        // According to Minesweeper rules, first reveal must be safe.
        // If not, re-locate the mine to make it safe.

        if (Moves == 0 && Board.Tiles[initialX, initialY].HasMine)
        {
            RelocateMine(initialX, initialY);
        }

        // Count moves.

        Moves++;

        // Set the initial tile's state.

        initialTile.SetRevealed();

        if (initialTile.HasMine)
        {
            // If the revealed tile was a mine, set game over and reveal all mines.

            State = GameState.Lost;

            RevealMines();

            return false;
        }
        else if (!initialTile.HasAdjacentMines())
        {
            // If the tile was blank (no adjacent mines) reveal safe area around the tile.

            RevealSafeAreaRecursively(initialTile);
        }

        // If all safe mines are revealed, the game is won.

        if (Revealed.Count() == _safe)
        {
            State = GameState.Won;
        }

        return true;

        // Method to reveal safe area around a tile.

        void RevealSafeAreaRecursively(Tile fromTile)
        {
            Board.ForEachAdjacentTo(fromTile, (tile) =>
            {
                if (tile.HasMine || tile.IsFlagged() || tile.IsRevealed())
                    return;

                tile.SetRevealed();

                if (!tile.HasAdjacentMines())
                    RevealSafeAreaRecursively(tile);
            });
        }
    }

    /// <summary>
    /// Reveal all mines.
    /// </summary>
    private void RevealMines()
    {
        foreach (var tile in Board.Tiles.Cast<Tile>())
        {
            if (tile.HasMine)
                tile.SetRevealed();
        }
    }

    /// <summary>
    /// Get a list of all revealed tiles, including mines.
    /// </summary>
    public IEnumerable<Tile> Revealed => Board.Tiles.Cast<Tile>().Where(t => t.IsRevealed());

    /// <summary>
    /// Flag a tile.
    /// </summary>
    public Tile Flag(int x, int y)
    {
        if (State != GameState.Started)
            throw new InvalidOperationException("Game is not started.");

        if (x >= Board.Columns || x < 0 || y >= Board.Rows || y < 0)
            throw new ArgumentOutOfRangeException("Given (x, y) out of bounds.");

        var tile = Board.Tiles[x, y];
        tile.SetFlagged();
        return tile;
    }

    /// <summary>
    /// Unflag a tile.
    /// </summary>
    public Tile Unflag(int x, int y)
    {
        if (State != GameState.Started)
            throw new InvalidOperationException("Game is not started.");

        if (x >= Board.Columns || x < 0 || y >= Board.Rows || y < 0)
            throw new ArgumentOutOfRangeException("Given (x, y) out of bounds.");

        var tile = Board.Tiles[x, y];
        tile.SetHidden();
        return tile;
    }
}

Board

public interface IBoard
{
    Tile[,] Tiles { get; }
    int Columns { get; }
    int Rows { get; }
    void ForEachAdjacentTo(Tile tile, Action<Tile> action);
}

public class Board : IBoard
{
    public Tile[,] Tiles { get; private set; }
    public int Columns => Tiles.GetLength(0);
    public int Rows => Tiles.GetLength(1);

    public Board(int columns, int rows)
    {
        InstantiateTiles(columns, rows);
    }

    /// <summary>
    /// Instantiate tiles.
    /// </summary>
    private void InstantiateTiles(int columns, int rows)
    {
        Tiles = new Tile[columns, rows];

        for (int x = 0; x < columns; x++)
            for (int y = 0; y < rows; y++)
                Tiles[x, y] = new Tile(x, y);
    }

    /// <summary>
    /// Invoke an action on each tile adjacent to the given tile.
    /// </summary>
    public void ForEachAdjacentTo(Tile tile, Action<Tile> action)
    {
        for (int ny = tile.Y - 1; ny <= tile.Y + 1; ny++)
        {
            if (ny < 0 || ny == Rows)
                continue;

            for (int nx = tile.X - 1; nx <= tile.X + 1; nx++)
            {
                if (nx < 0 || nx == Columns)
                    continue;

                if (nx == tile.X && ny == tile.Y)
                    continue;

                action.Invoke(Tiles[nx, ny]);
            }
        }
    }
}

public static partial class BoardExtensions
{
    /// <summary>
    /// Get a text visualization of the current state of the board.
    /// </summary>
    public static string PrintCurrentState(this Board board)
    {
        var sb = new StringBuilder();
        var orderedTiles = board.Tiles.Cast<Tile>().OrderBy(t => t.Y).ThenBy(t => t.X);

        foreach (var tile in orderedTiles)
        {
            if (!tile.IsRevealed())
                sb.Append(" ");
            else if (tile.IsFlagged())
                sb.Append(">");
            else if (tile.HasMine)
                sb.Append("x");
            else if (!tile.HasAdjacentMines())
                sb.Append("_");
            else
                sb.Append(tile.AdjacentMines.ToString());

            if (tile.X == board.Columns - 1)
                sb.Append("\n");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Get a text visualization of everything on the board, including hidden tiles.
    /// </summary>
    public static string PrintEverything(this Board board)
    {
        var sb = new StringBuilder();
        var orderedTiles = board.Tiles.Cast<Tile>().OrderBy(t => t.Y).ThenBy(t => t.X);

        foreach (var tile in orderedTiles)
        {
            if (tile.HasMine)
                sb.Append("x");
            else if (!tile.HasAdjacentMines())
                sb.Append("_");
            else
                sb.Append(tile.AdjacentMines.ToString());

            if (tile.X == board.Columns - 1)
                sb.Append("\n");
        }

        return sb.ToString();
    }
}

Tile

public enum TileState
{
    Revealed,
    Flagged,
    Hidden
}

public class Tile
{
    public Tile(int x, int y)
    {
        X = x;
        Y = y;
    }

    public int X { get; }
    public int Y { get; }
    public int AdjacentMines { get; internal set; }
    public bool HasMine { get; internal set; }
    public TileState State { get; internal set; } = TileState.Hidden;
}

public static partial class TileExtensions
{
    public static bool HasAdjacentMines(this Tile tile) => tile.AdjacentMines != 0;
    public static bool IsRevealed(this Tile tile) => tile.State == TileState.Revealed;
    public static bool IsFlagged(this Tile tile) => tile.State == TileState.Flagged;
    public static bool IsHidden(this Tile tile) => tile.State == TileState.Hidden;
    internal static void SetRevealed(this Tile tile) => tile.State = TileState.Revealed;
    internal static void SetFlagged(this Tile tile) => tile.State = TileState.Flagged;
    internal static void SetHidden(this Tile tile) => tile.State = TileState.Hidden;
}

GameTest

[TestClass]
public class GameTest
{
    /// <summary>
    /// Check initial state of game, before start and after started.
    /// Check correct number of tiles and mines are placed on the board.
    /// </summary>
    [TestMethod]
    public void Initial_game()
    {
        var game = new Game();

        // Check initial state and started state.

        Assert.AreEqual(GameState.NotStarted, game.State);

        game.NewGame(16, 16, 40);

        Assert.AreEqual(GameState.Started, game.State);

        var tileList = game.Board.Tiles.Cast<Tile>();

        // Check actual number of tiles.

        Assert.AreEqual(16 * 16, tileList.Count());

        int safeCounter = 0;
        int mineCounter = 0;

        foreach (var tile in tileList)
        {
            // Check initial tile state.

            Assert.AreEqual(TileState.Hidden, tile.State);

            // Count actual number of safe tiles and mines.

            if (!tile.HasMine)
                safeCounter++;
            else
                mineCounter++;

            // Count adjacent mines and check the property is correct.

            int adjacentMineCounter = 0;

            game.Board.ForEachAdjacentTo(tile, (a) =>
            {
                if (a.HasMine)
                    adjacentMineCounter++;
            });

            Assert.AreEqual(adjacentMineCounter, tile.AdjacentMines);
        }

        var tiles = 16 * 16;
        var mines = 40;
        var safe = tiles - mines;

        // Check actual number of safe and mines.

        Assert.AreEqual(safe, safeCounter);
        Assert.AreEqual(mines, mineCounter);
    }

    /// <summary>
    /// Win a game and check game state. Check that can't reveal more tiles after game is over.
    /// </summary>
    [TestMethod]
    public void Win_game()
    {
        var game = new Game();

        Assert.AreEqual(GameState.NotStarted, game.State);

        game.NewGame(16, 16, 40);

        Assert.AreEqual(GameState.Started, game.State);

        var safeList = game.Board.Tiles.Cast<Tile>().Where(t => !t.HasMine);

        foreach (var tile in safeList)
        {
            if (!game.Revealed.Contains(tile))
            {
                Assert.AreEqual(GameState.Started, game.State);
                game.Reveal(tile.X, tile.Y);
            }
        }

        Assert.AreEqual(GameState.Won, game.State);

        var randomTile = game.Board.Tiles.Cast<Tile>().Where(t => !t.IsRevealed()).First();

        Assert.ThrowsException<InvalidOperationException>(() => game.Reveal(randomTile.X, randomTile.Y));
    }

    /// <summary>
    /// Lose a game and check game state. Check that can't reveal more tiles after game is over.
    /// </summary>
    [TestMethod]
    public void Lose_game()
    {
        var game = new Game();

        Assert.AreEqual(GameState.NotStarted, game.State);

        game.NewGame(16, 16, 40);

        Assert.AreEqual(GameState.Started, game.State);

        var safeList = game.Board.Tiles.Cast<Tile>().Where(t => !t.HasMine);
        var mineList = game.Board.Tiles.Cast<Tile>().Where(t => t.HasMine);

        // Start by revealing a safe mine, or else it will be relocated.

        var firstSafe = safeList.First();
        game.Reveal(firstSafe.X, firstSafe.Y);

        Assert.AreEqual(GameState.Started, game.State);

        var firstMine = mineList.First();
        game.Reveal(firstMine.X, firstMine.Y);

        Assert.AreEqual(GameState.Lost, game.State);

        var randomTile = game.Board.Tiles.Cast<Tile>().Where(t => !t.IsRevealed()).First();

        Assert.ThrowsException<InvalidOperationException>(() => game.Reveal(randomTile.X, randomTile.Y));
    }

    /// <summary>
    /// Reveal safe tiles and check the state of revealed tiles.
    /// </summary>
    [TestMethod]
    public void Reveal_safe_tiles()
    {
        var game = new Game();

        game.NewGame(16, 16, 40);

        var safeList = game.Board.Tiles.Cast<Tile>().Where(t => !t.HasMine);

        foreach (var tile in safeList)
        {
            if (!tile.IsRevealed())
            {
                game.Reveal(tile.X, tile.Y);
            }
        }

        var revealed = game.Revealed;

        foreach (var r in revealed)
        {
            Assert.AreEqual(TileState.Revealed, r.State);
            Assert.IsFalse(r.HasMine);
        }
    }

    /// <summary>
    /// Reveal a mine and check the state of revealed tiles with mines.
    /// </summary>
    [TestMethod]
    public void Reveal_mines()
    {
        var game = new Game();

        game.NewGame(16, 16, 40);

        var safeList = game.Board.Tiles.Cast<Tile>().Where(t => !t.HasMine);
        var mineList = game.Board.Tiles.Cast<Tile>().Where(t => t.HasMine);

        // Start by revealing a safe mine, or else it will be relocated.

        var firstSafe = safeList.First();
        var wasSafe = game.Reveal(firstSafe.X, firstSafe.Y);

        Assert.IsTrue(wasSafe);

        var firstMine = mineList.First();
        var wasMine = !game.Reveal(firstMine.X, firstMine.Y);

        Assert.IsTrue(wasMine);

        var revealedMines = game.Revealed.Where(r => r.HasMine);

        Assert.AreEqual(40, revealedMines.Count());

        foreach (var r in revealedMines)
        {
            Assert.AreEqual(TileState.Revealed, r.State);
            Assert.IsTrue(r.HasMine);
        }
    }

    /// <summary>
    /// Set flags on tiles and check tile states. Remove flags then check tile states.
    /// </summary>
    [TestMethod]
    public void Flag_unflag()
    {
        var game = new Game();
        game.NewGame(5, 5, 1);

        game.Board.Tiles.Cast<Tile>().ToList().ForEach((tile) =>
        {
            game.Flag(tile.X, tile.Y);
            Assert.IsTrue(game.Board.Tiles[tile.X, tile.Y].IsFlagged());
        });

        game.Board.Tiles.Cast<Tile>().ToList().ForEach((tile) =>
        {
            game.Unflag(tile.X, tile.Y);
            Assert.IsFalse(game.Board.Tiles[tile.X, tile.Y].IsFlagged());
        });
    }

    /// <summary>
    /// Try to start a new game with invalid arguments.
    /// </summary>
    [TestMethod]
    public void Invalid_new_game_throws_exception()
    {
        var game = new Game();

        Assert.ThrowsException<ArgumentException>(() => game.NewGame(-1, 10, 5));
        Assert.ThrowsException<ArgumentException>(() => game.NewGame(10, 0, 5));
        Assert.ThrowsException<ArgumentException>(() => game.NewGame(10, 10, 5000));
    }

    /// <summary>
    /// Try to reveal tiles with invalid arguments.
    /// </summary>
    [TestMethod]
    public void Invalid_reveal_throws_exception()
    {
        var game = new Game();

        Assert.ThrowsException<InvalidOperationException>(() => game.Reveal(1, 1));

        game.NewGame(5, 5, 1);

        Assert.ThrowsException<ArgumentOutOfRangeException>(() => game.Reveal(4, -1));
        Assert.ThrowsException<ArgumentOutOfRangeException>(() => game.Reveal(17, 4));

        var safeList = game.Board.Tiles.Cast<Tile>().Where(t => !t.HasMine);

        var first = safeList.First();

        game.Reveal(first.X, first.Y);

        Assert.ThrowsException<InvalidOperationException>(() => game.Reveal(first.X, first.Y));
    }
}
\$\endgroup\$
  • \$\begingroup\$ As a consideration, you could make your Board and Tile class immutable. Create a struct called Move with x/y coordinates and the type of action (flag or click?). Then your Game can simply be a Board with a Queue of immutable Move structs. I think this approach would be easier to reason about, debug, and it allows you to do replays. \$\endgroup\$ – Brad M Aug 9 '18 at 13:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.