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.
153 lines
4.6 KiB
C#
153 lines
4.6 KiB
C#
using Minesweeper.Game;
|
|
using Xunit;
|
|
|
|
namespace Minesweeper.Tests;
|
|
|
|
public class MinesweeperGameTests
|
|
{
|
|
private static readonly string[] s_twinMineLayout =
|
|
[
|
|
"x.x",
|
|
"...",
|
|
"...",
|
|
];
|
|
|
|
private static readonly string[] s_cornerMineLayout =
|
|
[
|
|
"x..",
|
|
"...",
|
|
"...",
|
|
];
|
|
|
|
[Fact]
|
|
public void TheFirstClickPlantsMinesAndIsSafe()
|
|
{
|
|
var board = new Board(9, 9, 10);
|
|
var game = new MinesweeperGame(board, new Random(7));
|
|
|
|
Assert.False(game.HasOpenedFirstCell);
|
|
var result = game.Open(new CellRef(4, 4));
|
|
|
|
Assert.True(board.MinesPlanted);
|
|
Assert.True(game.HasOpenedFirstCell);
|
|
Assert.False(result.HitMine);
|
|
Assert.True(result.Opened.Count > 1, "the default safe zone should open an area");
|
|
Assert.Equal(GameStatus.Playing, game.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void EverySafeSquareOpeningWinsAndFlagsTheMines()
|
|
{
|
|
var game = TestBoards.Game(1, s_cornerMineLayout);
|
|
var result = game.Open(new CellRef(2, 0));
|
|
|
|
Assert.Equal(8, result.Opened.Count);
|
|
Assert.Equal(GameStatus.Won, game.Status);
|
|
Assert.Equal(1, game.FlagsPlaced);
|
|
Assert.Equal(0, game.MinesRemaining);
|
|
Assert.All(game.Board.MineCells(), mine => Assert.True(game.Board.At(mine).IsFlagged));
|
|
}
|
|
|
|
[Fact]
|
|
public void SteppingOnAMineEndsTheRoundAndShowsTheBoard()
|
|
{
|
|
var game = TestBoards.SingleMine(4, 4);
|
|
game.ToggleFlag(new CellRef(3, 3));
|
|
|
|
var result = game.Open(new CellRef(0, 0));
|
|
|
|
Assert.True(result.HitMine);
|
|
Assert.Equal(GameStatus.Lost, game.Status);
|
|
Assert.Equal(new CellRef(0, 0), game.Explosion!.Value);
|
|
Assert.True(game.Board.At(0, 0).Exploded);
|
|
Assert.All(game.Board.MineCells(), mine => Assert.True(game.Board.At(mine).IsRevealed || game.Board.At(mine).IsFlagged));
|
|
|
|
// A flag on safe ground stays put so the renderer can mark it as wrong.
|
|
Assert.True(game.Board.At(3, 3).IsFlagged);
|
|
Assert.False(game.Board.At(3, 3).IsMine);
|
|
}
|
|
|
|
[Fact]
|
|
public void FinishedRoundsIgnoreFurtherMoves()
|
|
{
|
|
var game = TestBoards.SingleMine();
|
|
game.Open(new CellRef(0, 0));
|
|
var revealed = game.RevealedCount;
|
|
|
|
Assert.False(game.Open(new CellRef(2, 2)).AnyOpened);
|
|
Assert.False(game.ToggleFlag(new CellRef(2, 3)));
|
|
Assert.Equal(revealed, game.RevealedCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void FlagsMoveTheCounterBothWays()
|
|
{
|
|
var game = TestBoards.SingleMine();
|
|
Assert.Equal(1, game.MinesRemaining);
|
|
|
|
Assert.True(game.ToggleFlag(new CellRef(1, 1)));
|
|
Assert.Equal(0, game.MinesRemaining);
|
|
|
|
Assert.True(game.ToggleFlag(new CellRef(2, 2)));
|
|
Assert.Equal(-1, game.MinesRemaining);
|
|
|
|
Assert.True(game.ToggleFlag(new CellRef(1, 1)));
|
|
Assert.Equal(0, game.MinesRemaining);
|
|
Assert.True(game.ToggleFlag(new CellRef(1, 1)), "a cleared square can be flagged again");
|
|
Assert.Equal(-1, game.MinesRemaining);
|
|
}
|
|
|
|
[Fact]
|
|
public void FlaggedSquaresCannotBeOpened()
|
|
{
|
|
var game = TestBoards.SingleMine();
|
|
game.ToggleFlag(new CellRef(1, 1));
|
|
|
|
Assert.False(game.Open(new CellRef(1, 1)).AnyOpened);
|
|
Assert.True(game.Board.At(1, 1).IsFlagged);
|
|
}
|
|
|
|
[Fact]
|
|
public void QuestionMarksAreOptionalAndStayOpenable()
|
|
{
|
|
var game = TestBoards.SingleMine();
|
|
|
|
Assert.False(game.ToggleQuestion(new CellRef(1, 1), questionMarksEnabled: false));
|
|
|
|
Assert.True(game.ToggleQuestion(new CellRef(1, 1), questionMarksEnabled: true));
|
|
Assert.Equal(CellState.Question, game.Board.At(1, 1).State);
|
|
|
|
Assert.True(game.Open(new CellRef(1, 1)).AnyOpened);
|
|
Assert.True(game.Board.At(1, 1).IsRevealed);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChordingThroughTheGameCountsTheNewSquares()
|
|
{
|
|
var game = TestBoards.Game(1, s_twinMineLayout);
|
|
game.Open(new CellRef(1, 0));
|
|
game.ToggleFlag(new CellRef(0, 0));
|
|
game.ToggleFlag(new CellRef(2, 0));
|
|
|
|
var result = game.Chord(new CellRef(1, 0));
|
|
|
|
Assert.Equal(3, result.Opened.Count);
|
|
Assert.Equal(4, game.RevealedCount);
|
|
Assert.Equal(GameStatus.Playing, game.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void RevealSolutionOnlyWorksAfterTheRound()
|
|
{
|
|
var game = TestBoards.SingleMine();
|
|
game.RevealSolution();
|
|
|
|
Assert.False(game.Board.At(0, 0).IsRevealed);
|
|
|
|
game.Open(new CellRef(0, 0));
|
|
game.RevealSolution();
|
|
|
|
Assert.True(game.Board.At(0, 0).IsRevealed);
|
|
}
|
|
}
|