Initial commit: terminal Minesweeper in C# for .NET 10
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.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user