Rules and board state live in Minesweeper.Core with no console calls, so the same board drives both the CLI and the tests.
Engine: seeded or random boards with classic presets; first click always safe (and its ring too when the board has room); cascading reveal that stops at numbers; flagging and chording; win by clearing every safe cell; FromMines() for deterministic layouts.
CLI: typed commands ("3 4", "f 2 5", "chord 6 7") plus optional arrow-key mode. Rows numbered, columns lettered, player input 1-based while engine coordinates stay 0-based, with CommandParser the only place that converts. ANSI colour when writing to a terminal, plain when redirected or NO_COLOR is set.
Tests: 186 covering flood-fill bounds, first-click safety, chording, win/loss, command parsing, session behaviour, and renderer alignment with colour on and off.
Co-authored-by: openhands <openhands@all-hands.dev>
576 lines
19 KiB
C#
576 lines
19 KiB
C#
using Minesweeper.Core;
|
|
|
|
namespace Minesweeper.Core.Tests;
|
|
|
|
public class MinesweeperGameTests
|
|
{
|
|
private static readonly CellLocation Mine = new(0, 0);
|
|
private static readonly CellLocation Numbered = new(0, 1);
|
|
private static readonly CellLocation SecondNumbered = new(1, 1);
|
|
private static readonly CellLocation FarCorner = new(2, 3);
|
|
|
|
/// <summary>3x4 board where only the far corner reads zero, so floods stay small and chording
|
|
/// matters.
|
|
/// <code>
|
|
/// M 1 1 M
|
|
/// 2 2 2 1
|
|
/// 1 M 1 0
|
|
/// </code></summary>
|
|
private static MinesweeperGame PuzzleBoard() =>
|
|
MinesweeperGame.FromMines(new BoardSize(3, 4, 3), [new CellLocation(0, 0), new CellLocation(0, 3), new CellLocation(2, 1)]);
|
|
|
|
/// <summary>2x3 board with a single mine, small enough to win with one chord.
|
|
/// <code>
|
|
/// 0 1 M
|
|
/// 0 1 1
|
|
/// </code></summary>
|
|
private static MinesweeperGame ChordBoard() =>
|
|
MinesweeperGame.FromMines(new BoardSize(2, 3, 1), [new CellLocation(0, 2)]);
|
|
|
|
[Theory]
|
|
[InlineData(0, 5, 3)]
|
|
[InlineData(5, 0, 3)]
|
|
[InlineData(2, 2, 4)]
|
|
[InlineData(2, 2, 0)]
|
|
[InlineData(3, 3, -1)]
|
|
public void Constructor_RejectsImpossibleBoards(int rows, int columns, int mines) =>
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new MinesweeperGame(new BoardSize(rows, columns, mines)));
|
|
|
|
[Theory]
|
|
[InlineData(GameDifficulty.Beginner, 9, 9, 10)]
|
|
[InlineData(GameDifficulty.Intermediate, 16, 16, 40)]
|
|
[InlineData(GameDifficulty.Expert, 16, 30, 99)]
|
|
public void BoardSize_For_HasClassicDimensions(GameDifficulty difficulty, int rows, int columns, int mines)
|
|
{
|
|
var size = BoardSize.For(difficulty);
|
|
|
|
Assert.Equal(rows, size.Rows);
|
|
Assert.Equal(columns, size.Columns);
|
|
Assert.Equal(mines, size.Mines);
|
|
Assert.Equal(rows * columns, size.CellCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void NewGame_StartsWithEverythingHiddenAndNoMinesPlaced()
|
|
{
|
|
var game = new MinesweeperGame(new BoardSize(5, 5, 4), seed: 1);
|
|
|
|
Assert.Equal(GameStatus.Ready, game.Status);
|
|
Assert.True(game.IsFirstMove);
|
|
Assert.Equal(0, game.RevealedCount);
|
|
Assert.Equal(25, game.UnrevealedCount);
|
|
Assert.Equal(0, game.FlagsUsed);
|
|
Assert.Empty(game.Mines);
|
|
Assert.Null(game.LostOn);
|
|
Assert.All(AllLocations(5, 5), l => Assert.Equal(CellState.Hidden, game.GetState(l)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(37)]
|
|
[InlineData(9999)]
|
|
public void FirstClick_IsNeverAMine(int seed)
|
|
{
|
|
var game = new MinesweeperGame(new BoardSize(9, 9, 10), seed);
|
|
var clicked = new CellLocation(4, 4);
|
|
|
|
game.Reveal(clicked);
|
|
|
|
Assert.False(game.IsMine(clicked));
|
|
Assert.Equal(CellState.Revealed, game.GetState(clicked));
|
|
Assert.DoesNotContain(clicked, game.Mines);
|
|
Assert.False(game.IsFirstMove);
|
|
}
|
|
|
|
[Fact]
|
|
public void FirstClick_KeepsNeighboursSafeWhenBoardHasRoom()
|
|
{
|
|
var game = new MinesweeperGame(new BoardSize(5, 5, 4), seed: 3);
|
|
var clicked = new CellLocation(2, 2);
|
|
|
|
game.Reveal(clicked);
|
|
|
|
Assert.All(game.Neighbours(clicked), neighbour => Assert.False(game.IsMine(neighbour)));
|
|
Assert.Equal(0, game.GetAdjacentMineCount(clicked));
|
|
}
|
|
|
|
[Fact]
|
|
public void FirstClick_RelaxesNeighbourSafetyOnDenseBoards()
|
|
{
|
|
// Every cell touches the centre of a 3x3 board, so only the clicked cell can stay safe.
|
|
var game = new MinesweeperGame(new BoardSize(3, 3, 8), seed: 5);
|
|
var clicked = new CellLocation(1, 1);
|
|
|
|
game.Reveal(clicked);
|
|
|
|
Assert.Equal(8, game.Mines.Count);
|
|
Assert.False(game.IsMine(clicked));
|
|
Assert.Equal(GameStatus.Won, game.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void FirstClick_SeedsExactlyTheRequestedMineCount()
|
|
{
|
|
var game = new MinesweeperGame(new BoardSize(16, 30, 99), seed: 12);
|
|
|
|
game.Reveal(new CellLocation(8, 15));
|
|
|
|
Assert.Equal(99, game.Mines.Count);
|
|
Assert.Equal(99, game.Mines.Distinct().Count());
|
|
Assert.All(game.Mines, mine => Assert.True(game.IsMine(mine)));
|
|
}
|
|
|
|
[Fact]
|
|
public void AdjacentMineCount_MatchesNeighbouringMines()
|
|
{
|
|
var game = new MinesweeperGame(new BoardSize(9, 9, 10), seed: 21);
|
|
game.Reveal(new CellLocation(4, 4));
|
|
|
|
foreach (var location in AllLocations(9, 9))
|
|
{
|
|
Assert.Equal(game.Neighbours(location).Count(game.IsMine), game.GetAdjacentMineCount(location));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SameSeed_ProducesIdenticalBoard()
|
|
{
|
|
var first = new MinesweeperGame(new BoardSize(9, 9, 10), seed: 77);
|
|
var second = new MinesweeperGame(new BoardSize(9, 9, 10), seed: 77);
|
|
|
|
first.Reveal(new CellLocation(0, 0));
|
|
second.Reveal(new CellLocation(0, 0));
|
|
|
|
Assert.Equal(first.Mines, second.Mines);
|
|
}
|
|
|
|
[Fact]
|
|
public void DifferentSeeds_ProduceDifferentBoards()
|
|
{
|
|
var layouts = Enumerable.Range(0, 12)
|
|
.Select(seed =>
|
|
{
|
|
var game = new MinesweeperGame(new BoardSize(9, 9, 10), seed);
|
|
game.Reveal(new CellLocation(4, 4));
|
|
return game.Mines;
|
|
})
|
|
.Distinct()
|
|
.Count();
|
|
|
|
Assert.True(layouts > 1, "Expected distinct layouts across twelve seeds.");
|
|
}
|
|
|
|
/// <summary>5x5 board whose mines form a partial wall down column 2, so a click in the corner
|
|
/// opens exactly the ten cells to the left of it and nothing beyond.
|
|
/// <code>
|
|
/// 0 1 M 1 0
|
|
/// 0 2 M 2 1
|
|
/// 0 2 M 2 1
|
|
/// 0 2 M 2 1
|
|
/// 0 1 1 1 0
|
|
/// </code></summary>
|
|
private static MinesweeperGame WalledBoard() => MinesweeperGame.FromMines(new BoardSize(5, 5, 4),
|
|
[new CellLocation(0, 2), new CellLocation(1, 2), new CellLocation(2, 2), new CellLocation(3, 2)]);
|
|
|
|
[Fact]
|
|
public void Reveal_EmptyRegion_FloodsUntilNumbers()
|
|
{
|
|
var game = WalledBoard();
|
|
|
|
var revealed = game.Reveal(new CellLocation(0, 0));
|
|
|
|
Assert.Equal(10, revealed.Count);
|
|
Assert.All(revealed, l => Assert.Equal(CellState.Revealed, game.GetState(l)));
|
|
Assert.All(revealed, l => Assert.False(game.IsMine(l)));
|
|
Assert.Equal(GameStatus.Playing, game.Status);
|
|
|
|
// The numbers along the wall open, but the flood does not continue past them.
|
|
Assert.Equal(CellState.Revealed, game.GetState(new CellLocation(4, 1)));
|
|
Assert.Equal(1, game.GetAdjacentMineCount(new CellLocation(4, 1)));
|
|
Assert.All(AllLocations(5, 5).Where(l => l.Column >= 3), l => Assert.Equal(CellState.Hidden, game.GetState(l)));
|
|
Assert.All(game.Mines, mine => Assert.Equal(CellState.Hidden, game.GetState(mine)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_FloodStopsAtNumbers()
|
|
{
|
|
var game = MinesweeperGame.FromMines(new BoardSize(1, 5, 1), [new CellLocation(0, 4)]);
|
|
|
|
var revealed = game.Reveal(new CellLocation(0, 0));
|
|
|
|
Assert.Equal(
|
|
[new CellLocation(0, 0), new CellLocation(0, 1), new CellLocation(0, 2), new CellLocation(0, 3)],
|
|
revealed.OrderBy(l => l.Column));
|
|
Assert.Equal(1, game.GetAdjacentMineCount(new CellLocation(0, 3)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_FloodDoesNotOpenFlaggedCells()
|
|
{
|
|
var game = MinesweeperGame.FromMines(new BoardSize(5, 5, 1), [new CellLocation(4, 4)]);
|
|
game.ToggleFlag(new CellLocation(2, 2));
|
|
|
|
var revealed = game.Reveal(new CellLocation(0, 0));
|
|
|
|
Assert.DoesNotContain(new CellLocation(2, 2), revealed);
|
|
Assert.Equal(CellState.Flagged, game.GetState(new CellLocation(2, 2)));
|
|
Assert.Equal(1, game.FlagsUsed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_OnNumberedCell_RevealsOnlyItself()
|
|
{
|
|
var game = PuzzleBoard();
|
|
|
|
var revealed = game.Reveal(Numbered);
|
|
|
|
Assert.Equal([Numbered], revealed);
|
|
Assert.Equal(1, game.GetAdjacentMineCount(Numbered));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_AlreadyRevealedCell_IsANoOp()
|
|
{
|
|
var game = PuzzleBoard();
|
|
game.Reveal(Numbered);
|
|
|
|
Assert.Empty(game.Reveal(Numbered));
|
|
Assert.Equal(GameStatus.Playing, game.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_FlaggedCell_IsANoOp()
|
|
{
|
|
var game = PuzzleBoard();
|
|
game.ToggleFlag(Numbered);
|
|
|
|
Assert.Empty(game.Reveal(Numbered));
|
|
Assert.Equal(CellState.Flagged, game.GetState(Numbered));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_OutsideBoard_Throws()
|
|
{
|
|
var game = PuzzleBoard();
|
|
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => game.Reveal(new CellLocation(3, 0)));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => game.Reveal(new CellLocation(-1, 0)));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => game.ToggleFlag(new CellLocation(0, 4)));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => game.Chord(new CellLocation(0, -3)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_OnMine_LosesAndExposesEveryMine()
|
|
{
|
|
var mines = new[] { new CellLocation(0, 0), new CellLocation(2, 2) };
|
|
var game = MinesweeperGame.FromMines(new BoardSize(3, 3, 2), mines);
|
|
|
|
var revealed = game.Reveal(new CellLocation(2, 2));
|
|
|
|
Assert.Equal(GameStatus.Lost, game.Status);
|
|
Assert.Equal(new CellLocation(2, 2), game.LostOn);
|
|
Assert.Equal([new CellLocation(2, 2)], revealed);
|
|
Assert.All(mines, l => Assert.Equal(CellState.Revealed, game.GetState(l)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_OnMine_MarksIncorrectFlagsAsWrong()
|
|
{
|
|
var game = MinesweeperGame.FromMines(new BoardSize(3, 3, 1), [Mine]);
|
|
game.ToggleFlag(new CellLocation(2, 2));
|
|
|
|
game.Reveal(Mine);
|
|
|
|
Assert.Equal(GameStatus.Lost, game.Status);
|
|
Assert.Equal(CellState.WrongFlag, game.GetState(new CellLocation(2, 2)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_LastSafeCell_WinsAndAutoFlagsMines()
|
|
{
|
|
var game = PuzzleBoard();
|
|
var mines = game.Mines.ToHashSet();
|
|
var safeCells = AllLocations(3, 4).Where(l => !mines.Contains(l)).ToList();
|
|
|
|
foreach (var cell in safeCells.Take(safeCells.Count - 1))
|
|
{
|
|
if (game.GetState(cell) == CellState.Hidden)
|
|
{
|
|
game.Reveal(cell);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(GameStatus.Playing, game.Status);
|
|
|
|
game.Reveal(safeCells[^1]);
|
|
|
|
Assert.Equal(GameStatus.Won, game.Status);
|
|
Assert.Equal(9, game.RevealedCount);
|
|
Assert.Equal(3, game.FlagsUsed);
|
|
Assert.All(mines, mine => Assert.Equal(CellState.Flagged, game.GetState(mine)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reveal_FloodCoveringAllSafeCells_Wins()
|
|
{
|
|
var game = MinesweeperGame.FromMines(new BoardSize(3, 3, 1), [new CellLocation(2, 2)]);
|
|
|
|
game.Reveal(new CellLocation(0, 0));
|
|
|
|
Assert.Equal(GameStatus.Won, game.Status);
|
|
Assert.Equal(8, game.RevealedCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Move_AfterWin_ThrowsGameOver()
|
|
{
|
|
var game = MinesweeperGame.FromMines(new BoardSize(3, 3, 1), [new CellLocation(2, 2)]);
|
|
game.Reveal(new CellLocation(0, 0));
|
|
|
|
var exception = Assert.Throws<GameOverException>(() => game.Reveal(new CellLocation(0, 1)));
|
|
Assert.Equal(GameStatus.Won, exception.Status);
|
|
Assert.Throws<GameOverException>(() => game.ToggleFlag(new CellLocation(0, 1)));
|
|
Assert.Throws<GameOverException>(() => game.Chord(new CellLocation(0, 1)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Move_AfterLoss_ThrowsGameOver()
|
|
{
|
|
var game = MinesweeperGame.FromMines(new BoardSize(3, 3, 1), [Mine]);
|
|
game.Reveal(Mine);
|
|
|
|
var exception = Assert.Throws<GameOverException>(() => game.Reveal(new CellLocation(2, 2)));
|
|
Assert.Equal(GameStatus.Lost, exception.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToggleFlag_RoundTripsThroughHidden()
|
|
{
|
|
var game = PuzzleBoard();
|
|
|
|
Assert.True(game.ToggleFlag(Numbered));
|
|
Assert.Equal(CellState.Flagged, game.GetState(Numbered));
|
|
Assert.Equal(1, game.FlagsUsed);
|
|
|
|
Assert.True(game.ToggleFlag(Numbered));
|
|
Assert.Equal(CellState.Hidden, game.GetState(Numbered));
|
|
Assert.Equal(0, game.FlagsUsed);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToggleFlag_OnRevealedCell_IsRejected()
|
|
{
|
|
var game = PuzzleBoard();
|
|
game.Reveal(Numbered);
|
|
|
|
Assert.False(game.ToggleFlag(Numbered));
|
|
Assert.Equal(CellState.Revealed, game.GetState(Numbered));
|
|
Assert.Equal(0, game.FlagsUsed);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToggleFlag_DoesNotChangeRevealedCount()
|
|
{
|
|
var game = PuzzleBoard();
|
|
game.Reveal(FarCorner);
|
|
var before = game.RevealedCount;
|
|
|
|
game.ToggleFlag(Mine);
|
|
|
|
Assert.Equal(before, game.RevealedCount);
|
|
Assert.Equal(1, game.FlagsUsed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Chord_OnHiddenCell_IsANoOp()
|
|
{
|
|
var game = PuzzleBoard();
|
|
|
|
Assert.Empty(game.Chord(Numbered));
|
|
}
|
|
|
|
[Fact]
|
|
public void Chord_WithTooFewFlags_IsANoOp()
|
|
{
|
|
var game = PuzzleBoard();
|
|
game.Reveal(Numbered);
|
|
|
|
Assert.Equal(1, game.GetAdjacentMineCount(Numbered));
|
|
Assert.Empty(game.Chord(Numbered));
|
|
Assert.Equal(GameStatus.Playing, game.Status);
|
|
Assert.Equal([Numbered], RevealedCells(AllLocations(3, 4), game));
|
|
}
|
|
|
|
/// <summary>Every revealed cell, used to assert a chord opened nothing extra.</summary>
|
|
private static List<CellLocation> RevealedCells(IEnumerable<CellLocation> locations, MinesweeperGame game) =>
|
|
locations.Where(l => game.GetState(l) == CellState.Revealed).ToList();
|
|
|
|
[Fact]
|
|
public void Chord_WithCorrectFlags_OpensRemainingNeighbours()
|
|
{
|
|
var game = ChordBoard();
|
|
game.Reveal(new CellLocation(1, 0));
|
|
game.ToggleFlag(new CellLocation(0, 2));
|
|
|
|
var opened = game.Chord(new CellLocation(0, 1));
|
|
|
|
Assert.Equal([new CellLocation(1, 2)], opened);
|
|
Assert.Equal(GameStatus.Won, game.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void Chord_OnMistakenFlag_LosesGame()
|
|
{
|
|
var game = ChordBoard();
|
|
game.Reveal(new CellLocation(1, 0));
|
|
game.ToggleFlag(new CellLocation(1, 2));
|
|
|
|
game.Chord(new CellLocation(0, 1));
|
|
|
|
Assert.Equal(GameStatus.Lost, game.Status);
|
|
Assert.Equal(new CellLocation(0, 2), game.LostOn);
|
|
Assert.Equal(CellState.WrongFlag, game.GetState(new CellLocation(1, 2)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Chord_OnlyCountsItsOwnAdjacentFlags()
|
|
{
|
|
var game = PuzzleBoard();
|
|
game.Reveal(Numbered);
|
|
game.ToggleFlag(new CellLocation(0, 3));
|
|
|
|
// The flag sits on the far mine, which does not touch (0,1), so the counts still mismatch.
|
|
Assert.DoesNotContain(new CellLocation(0, 3), game.Neighbours(Numbered));
|
|
Assert.Empty(game.Chord(Numbered));
|
|
}
|
|
|
|
[Fact]
|
|
public void Chord_OnZeroCell_OpensNothingNewAfterItsFlood()
|
|
{
|
|
var game = PuzzleBoard();
|
|
game.Reveal(FarCorner);
|
|
|
|
Assert.Equal(0, game.GetAdjacentMineCount(FarCorner));
|
|
Assert.Empty(game.Chord(FarCorner));
|
|
}
|
|
|
|
[Fact]
|
|
public void Neighbours_RespectBoardEdges()
|
|
{
|
|
var game = PuzzleBoard();
|
|
|
|
Assert.Equal(3, game.Neighbours(new CellLocation(0, 0)).Count);
|
|
Assert.Equal(5, game.Neighbours(new CellLocation(0, 1)).Count);
|
|
Assert.Equal(8, game.Neighbours(new CellLocation(1, 1)).Count);
|
|
Assert.All(game.Neighbours(new CellLocation(0, 0)), n => Assert.True(game.IsWithinBounds(n)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Neighbours_NeverIncludeTheCellItself()
|
|
{
|
|
var game = PuzzleBoard();
|
|
|
|
Assert.DoesNotContain(SecondNumbered, game.Neighbours(SecondNumbered));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsWithinBounds_CoversExactlyTheGrid()
|
|
{
|
|
var game = new MinesweeperGame(new BoardSize(2, 4, 1), seed: 1);
|
|
|
|
Assert.True(game.IsWithinBounds(new CellLocation(1, 3)));
|
|
Assert.False(game.IsWithinBounds(new CellLocation(2, 0)));
|
|
Assert.False(game.IsWithinBounds(new CellLocation(1, 4)));
|
|
Assert.False(game.IsWithinBounds(new CellLocation(-1, 0)));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetView_ExposesPerCellData()
|
|
{
|
|
var game = MinesweeperGame.FromMines(new BoardSize(4, 5, 3),
|
|
[new CellLocation(0, 0), new CellLocation(2, 2), new CellLocation(3, 4)]);
|
|
game.Reveal(new CellLocation(0, 4));
|
|
game.ToggleFlag(new CellLocation(1, 0));
|
|
var view = game.GetView();
|
|
|
|
Assert.Equal(4, view.GetLength(0));
|
|
Assert.Equal(5, view.GetLength(1));
|
|
foreach (var location in AllLocations(4, 5))
|
|
{
|
|
var cell = view[location.Row, location.Column];
|
|
Assert.Equal(location, cell.Location);
|
|
Assert.Equal(game.IsMine(location), cell.IsMine);
|
|
Assert.Equal(game.GetState(location), cell.State);
|
|
Assert.Equal(game.GetAdjacentMineCount(location), cell.AdjacentMines);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void GetView_ReturnsAFreshSnapshotEachCall()
|
|
{
|
|
var game = MinesweeperGame.FromMines(new BoardSize(3, 3, 1), [new CellLocation(2, 2)]);
|
|
|
|
var view = game.GetView();
|
|
view[0, 0] = default;
|
|
|
|
Assert.Equal(CellState.Hidden, game.GetState(new CellLocation(0, 0)));
|
|
Assert.Equal(new CellLocation(0, 0), game.GetView()[0, 0].Location);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromMines_RejectsWrongMineCountOrBadLayout()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
MinesweeperGame.FromMines(new BoardSize(3, 3, 2), [new CellLocation(0, 0)]));
|
|
Assert.Throws<ArgumentException>(() =>
|
|
MinesweeperGame.FromMines(new BoardSize(3, 3, 1), [new CellLocation(5, 5)]));
|
|
Assert.Throws<ArgumentException>(() =>
|
|
MinesweeperGame.FromMines(new BoardSize(3, 3, 2), [new CellLocation(0, 0), new CellLocation(0, 0)]));
|
|
}
|
|
|
|
[Fact]
|
|
public void FromMines_StartsInPlayingStateWithNumbersComputed()
|
|
{
|
|
var game = PuzzleBoard();
|
|
|
|
Assert.Equal(GameStatus.Playing, game.Status);
|
|
Assert.False(game.IsFirstMove);
|
|
Assert.Equal(2, game.GetAdjacentMineCount(SecondNumbered));
|
|
Assert.Equal(0, game.GetAdjacentMineCount(FarCorner));
|
|
}
|
|
|
|
[Fact]
|
|
public void SolvingRandomBeginnerBoards_AlwaysWins()
|
|
{
|
|
for (var seed = 0; seed < 40; seed++)
|
|
{
|
|
var game = new MinesweeperGame(new BoardSize(9, 9, 10), seed);
|
|
game.Reveal(new CellLocation(4, 4));
|
|
var mines = game.Mines.ToHashSet();
|
|
|
|
foreach (var cell in AllLocations(9, 9).Where(l => !mines.Contains(l)))
|
|
{
|
|
if (game.GetState(cell) == CellState.Hidden)
|
|
{
|
|
game.Reveal(cell);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(GameStatus.Won, game.Status);
|
|
Assert.Equal(71, game.RevealedCount);
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<CellLocation> AllLocations(int rows, int columns)
|
|
{
|
|
for (var row = 0; row < rows; row++)
|
|
{
|
|
for (var column = 0; column < columns; column++)
|
|
{
|
|
yield return new CellLocation(row, column);
|
|
}
|
|
}
|
|
}
|
|
}
|