using Minesweeper.Game; using Xunit; namespace Minesweeper.Tests; public class BoardTests { private static readonly string[] s_floodLayout = [ ".....", ".....", ".....", ".....", "...x.", ]; private static readonly string[] s_twinMineLayout = [ "x.x", "...", "...", ]; [Theory] [InlineData(0, 5, 1)] [InlineData(5, 0, 1)] [InlineData(3, 3, -1)] [InlineData(3, 3, 9)] public void ConstructorRejectsImpossibleBoards(int width, int height, int mines) => Assert.ThrowsAny(() => new Board(width, height, mines)); [Fact] public void FirstClickKeepsTheChosenSquareClear() { for (var seed = 0; seed < 50; seed++) { var board = new Board(9, 9, 10); board.PlantMines(new CellRef(4, 4), reserveSafeZone: false, new Random(seed)); Assert.False(board.At(4, 4).IsMine); Assert.Equal(10, board.MineCount); Assert.Equal(10, board.MineCells().Count()); } } [Fact] public void FirstClickCanAlsoKeepItsNeighborhoodClear() { for (var seed = 0; seed < 50; seed++) { var board = new Board(9, 9, 10); board.PlantMines(new CellRef(4, 4), reserveSafeZone: true, new Random(seed)); Assert.Equal(0, board.MineCells().Count(m => m == new CellRef(4, 4) || board.NeighborsOf(4, 4).Contains(m))); } } [Fact] public void PlantingTwiceThrows() { var board = TestBoards.Create(s_floodLayout); Assert.Throws(() => board.PlantMines([])); Assert.Throws(() => board.PlantMines(new CellRef(0, 0), true, new Random(1))); } [Fact] public void NumbersComeFromTheMineLayout() { var board = TestBoards.Create(s_floodLayout); Assert.True(board.At(3, 4).IsMine); Assert.Equal(0, board.At(0, 0).AdjacentMines); Assert.Equal(1, board.At(2, 3).AdjacentMines); Assert.Equal(1, board.At(4, 4).AdjacentMines); Assert.Equal(1, board.MineCount); } [Theory] [InlineData(0, 0, 3)] [InlineData(2, 0, 5)] [InlineData(2, 2, 8)] public void NeighborCountsRespectTheEdges(int x, int y, int expected) { var board = new Board(5, 5, 0); Assert.Equal(expected, board.NeighborsOf(x, y).Count()); } [Fact] public void OpeningABlankSquareFloodsUntilTheNumbers() { var board = TestBoards.Create(s_floodLayout); var result = board.Open(new CellRef(0, 0)); Assert.False(result.HitMine); Assert.Equal(23, result.Opened.Count); Assert.False(board.At(3, 4).IsRevealed); Assert.True(board.At(2, 3).IsRevealed); Assert.Equal(23, Revealed(board)); // (4,4) only touches other numbers and the mine, so a flood can never reach it. Assert.False(board.At(4, 4).IsRevealed); } [Fact] public void FlagsHoldBackTheFloodFill() { var board = TestBoards.Create(s_floodLayout); board.At(0, 2).State = CellState.Flagged; var result = board.Open(new CellRef(0, 0)); Assert.Equal(22, result.Opened.Count); Assert.True(board.At(0, 2).IsFlagged); } [Fact] public void OpeningANumberedSquareRevealsOnlyIt() { var board = TestBoards.Create("x..", "...", "..."); var result = board.Open(new CellRef(1, 0)); Assert.Single(result.Opened); Assert.True(board.At(1, 0).IsRevealed); Assert.False(board.At(2, 2).IsRevealed); } [Fact] public void OpeningAMineReportsTheBoom() { var board = TestBoards.Create(s_floodLayout); var result = board.Open(new CellRef(3, 4)); Assert.True(result.HitMine); Assert.Equal(new CellRef(3, 4), result.Boom!.Value); Assert.True(board.At(3, 4).IsRevealed); } [Fact] public void ChordOpensTheRestWhenTheFlagsMatch() { var board = TestBoards.Create(s_twinMineLayout); board.Open(new CellRef(1, 0)); board.At(0, 0).State = CellState.Flagged; board.At(2, 0).State = CellState.Flagged; var result = board.Chord(new CellRef(1, 0)); Assert.False(result.HitMine); Assert.Equal(3, result.Opened.Count); Assert.True(board.At(0, 1).IsRevealed); Assert.True(board.At(1, 1).IsRevealed); Assert.True(board.At(2, 1).IsRevealed); } [Theory] [InlineData(1)] [InlineData(3)] public void ChordWithTheWrongFlagCountJustPops(int flags) { var board = TestBoards.Create(s_twinMineLayout); board.Open(new CellRef(1, 0)); board.At(0, 0).State = CellState.Flagged; if (flags > 1) board.At(0, 1).State = CellState.Flagged; if (flags > 2) board.At(1, 1).State = CellState.Flagged; var result = board.Chord(new CellRef(1, 0)); Assert.False(result.AnyOpened); Assert.False(result.HitMine); Assert.Equal(1, Revealed(board)); } [Fact] public void ChordCanStepOnAnUnflaggedMine() { var board = TestBoards.Create(s_twinMineLayout); board.Open(new CellRef(1, 0)); board.At(0, 0).State = CellState.Flagged; board.At(0, 1).State = CellState.Flagged; var result = board.Chord(new CellRef(1, 0)); Assert.True(result.HitMine); Assert.Equal(new CellRef(2, 0), result.Boom!.Value); } [Fact] public void ChordIgnoresHiddenSquares() { var board = TestBoards.Create(s_twinMineLayout); Assert.False(board.Chord(new CellRef(1, 1)).AnyOpened); } [Fact] public void TinyBoardsLoseMinesTheyCannotFit() { var board = new Board(2, 2, 3); board.PlantMines(new CellRef(0, 0), reserveSafeZone: true, new Random(3)); Assert.Equal(0, board.MineCount); Assert.Empty(board.MineCells()); } [Fact] public void OutOfRangeSquaresThrow() { var board = new Board(3, 3, 1); Assert.Throws(() => board.At(3, 0)); Assert.False(board.InBounds(-1, 2)); Assert.True(board.InBounds(2, 2)); } private static int Revealed(Board board) => board.AllCells().Count(reference => board.At(reference).IsRevealed); }