A playable console Minesweeper (arrow keys, chording, flags, question marks, pause, difficulty presets) plus a deduction-and-guess solver that can play the game headlessly. Game logic lives in src/Minesweeper/Game and has no console dependencies, so it is covered by 65 unit tests. Includes publishing helpers for self-contained, trimmed single-file binaries (publish.sh) and an .editorconfig that the sources conform to: UTF-8 BOM and 4-space indents for C#, 2-space indents for project files, LF endings and 2-space indents for shell scripts, System-first usings, no redundant `this.`, and s_/ _ prefixed private static and instance fields.
217 lines
6.3 KiB
C#
217 lines
6.3 KiB
C#
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<ArgumentOutOfRangeException>(() => 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<InvalidOperationException>(() => board.PlantMines([]));
|
|
Assert.Throws<InvalidOperationException>(() => 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<ArgumentOutOfRangeException>(() => 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);
|
|
}
|