326 lines
9.9 KiB
Go
326 lines
9.9 KiB
Go
package main
|
||
|
||
import (
|
||
"math/rand"
|
||
"testing"
|
||
)
|
||
|
||
func msp(r, c int) MinesweeperPos { return MinesweeperPos{Row: r, Col: c} }
|
||
|
||
func TestNewMinesweeperGameEmpty(t *testing.T) {
|
||
g := NewMinesweeperGame(9, 9, 10)
|
||
if g.Width != 9 || g.Height != 9 || g.MineCount != 10 {
|
||
t.Fatalf("неверные параметры поля: %+v", g)
|
||
}
|
||
for r := 0; r < 9; r++ {
|
||
for c := 0; c < 9; c++ {
|
||
if g.Board[r][c].IsMine || g.Board[r][c].Revealed {
|
||
t.Fatalf("новое поле не должно содержать мин или открытых клеток до первого хода")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMineCountClampedToLeaveOneSafeCell(t *testing.T) {
|
||
g := NewMinesweeperGame(3, 3, 100)
|
||
if g.MineCount != 8 {
|
||
t.Errorf("ожидалось ограничение до 8 мин (9 клеток - 1), получено %d", g.MineCount)
|
||
}
|
||
}
|
||
|
||
func TestFirstRevealNeverHitsMine(t *testing.T) {
|
||
rnd := rand.New(rand.NewSource(1))
|
||
for trial := 0; trial < 50; trial++ {
|
||
g := NewMinesweeperGame(9, 9, 10)
|
||
pos := msp(4, 4)
|
||
g.placeMines(pos, rnd)
|
||
if g.Board[pos.Row][pos.Col].IsMine {
|
||
t.Fatalf("попытка %d: первая открытая клетка не должна быть миной", trial)
|
||
}
|
||
for _, n := range msNeighbors(pos) {
|
||
if g.inBounds(n) && g.Board[n.Row][n.Col].IsMine {
|
||
t.Fatalf("попытка %d: сосед первой клетки тоже не должен быть миной", trial)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRevealPlacesMinesLazilyOnFirstCall(t *testing.T) {
|
||
g := NewMinesweeperGame(9, 9, 10)
|
||
if g.minesPlaced {
|
||
t.Fatalf("мины не должны быть расставлены до первого Reveal")
|
||
}
|
||
if err := g.Reveal(msp(4, 4)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if !g.minesPlaced {
|
||
t.Errorf("мины должны были расставиться после первого Reveal")
|
||
}
|
||
}
|
||
|
||
func TestRevealMineEndsGameAndRevealsAllMines(t *testing.T) {
|
||
g := NewMinesweeperGame(5, 5, 3)
|
||
g.placeMines(msp(0, 0), rand.New(rand.NewSource(1)))
|
||
g.minesPlaced = true
|
||
|
||
var minePos MinesweeperPos
|
||
found := false
|
||
for r := 0; r < 5 && !found; r++ {
|
||
for c := 0; c < 5 && !found; c++ {
|
||
if g.Board[r][c].IsMine {
|
||
minePos = msp(r, c)
|
||
found = true
|
||
}
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("в тестовом поле должна была быть хотя бы одна мина")
|
||
}
|
||
|
||
if err := g.Reveal(minePos); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if g.Result == nil || g.Result.Won {
|
||
t.Fatalf("ожидалось поражение после открытия мины")
|
||
}
|
||
for r := 0; r < 5; r++ {
|
||
for c := 0; c < 5; c++ {
|
||
if g.Board[r][c].IsMine && !g.Board[r][c].Revealed {
|
||
t.Errorf("все мины должны были раскрыться при поражении")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestFloodRevealOpensZeroRegion(t *testing.T) {
|
||
g := NewMinesweeperGame(5, 5, 1)
|
||
g.Board[4][4].IsMine = true
|
||
for r := 0; r < 5; r++ {
|
||
for c := 0; c < 5; c++ {
|
||
if g.Board[r][c].IsMine {
|
||
continue
|
||
}
|
||
count := 0
|
||
for _, n := range msNeighbors(msp(r, c)) {
|
||
if g.inBounds(n) && g.Board[n.Row][n.Col].IsMine {
|
||
count++
|
||
}
|
||
}
|
||
g.Board[r][c].AdjacentMines = count
|
||
}
|
||
}
|
||
g.minesPlaced = true
|
||
|
||
if err := g.Reveal(msp(0, 0)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if g.Board[0][0].AdjacentMines != 0 {
|
||
t.Fatalf("тестовая настройка неверна: (0,0) должна иметь 0 соседних мин")
|
||
}
|
||
if !g.Board[0][2].Revealed {
|
||
t.Errorf("каскад должен был открыть клетки вдалеке от мины")
|
||
}
|
||
if g.Board[4][4].Revealed {
|
||
t.Errorf("сама мина не должна была открыться просто от каскада рядом")
|
||
}
|
||
}
|
||
|
||
func TestCannotRevealAlreadyOpenCell(t *testing.T) {
|
||
g := NewMinesweeperGame(9, 9, 10)
|
||
g.Reveal(msp(4, 4))
|
||
if err := g.Reveal(msp(4, 4)); err != ErrMinesweeperAlreadyOpen {
|
||
t.Errorf("ожидалась ошибка повторного открытия, получено: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCannotRevealFlaggedCell(t *testing.T) {
|
||
g := NewMinesweeperGame(9, 9, 10)
|
||
g.ToggleFlag(msp(4, 4))
|
||
if err := g.Reveal(msp(4, 4)); err != ErrMinesweeperFlagged {
|
||
t.Errorf("ожидалась ошибка флага, получено: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestToggleFlagOnAndOff(t *testing.T) {
|
||
g := NewMinesweeperGame(9, 9, 10)
|
||
if err := g.ToggleFlag(msp(4, 4)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if !g.Board[4][4].Flagged || g.FlagCount != 1 {
|
||
t.Errorf("клетка должна была быть помечена флагом")
|
||
}
|
||
if err := g.ToggleFlag(msp(4, 4)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if g.Board[4][4].Flagged || g.FlagCount != 0 {
|
||
t.Errorf("флаг должен был сняться")
|
||
}
|
||
}
|
||
|
||
func TestCannotFlagRevealedCell(t *testing.T) {
|
||
g := NewMinesweeperGame(9, 9, 10)
|
||
g.Reveal(msp(4, 4))
|
||
if err := g.ToggleFlag(msp(4, 4)); err != ErrMinesweeperAlreadyOpen {
|
||
t.Errorf("ожидалась ошибка попытки пометить открытую клетку, получено: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestOutOfBoundsErrors(t *testing.T) {
|
||
g := NewMinesweeperGame(9, 9, 10)
|
||
if err := g.Reveal(msp(-1, 0)); err != ErrMinesweeperOutOfBounds {
|
||
t.Errorf("ожидалась ошибка выхода за границы, получено: %v", err)
|
||
}
|
||
if err := g.ToggleFlag(msp(0, 9)); err != ErrMinesweeperOutOfBounds {
|
||
t.Errorf("ожидалась ошибка выхода за границы, получено: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestWinConditionAllSafeCellsRevealed(t *testing.T) {
|
||
g := NewMinesweeperGame(2, 2, 1)
|
||
g.Board[1][1].IsMine = true
|
||
g.Board[0][0].AdjacentMines = 1
|
||
g.Board[0][1].AdjacentMines = 1
|
||
g.Board[1][0].AdjacentMines = 1
|
||
g.minesPlaced = true
|
||
|
||
g.Reveal(msp(0, 0))
|
||
if g.Result != nil {
|
||
t.Fatalf("после одной открытой клетки из трёх безопасных победа наступить не должна")
|
||
}
|
||
g.Reveal(msp(0, 1))
|
||
g.Reveal(msp(1, 0))
|
||
if g.Result == nil || !g.Result.Won {
|
||
t.Fatalf("после открытия всех безопасных клеток должна была наступить победа")
|
||
}
|
||
}
|
||
|
||
func TestActionsAfterGameOverReturnError(t *testing.T) {
|
||
g := NewMinesweeperGame(5, 5, 3)
|
||
g.placeMines(msp(0, 0), rand.New(rand.NewSource(1)))
|
||
g.minesPlaced = true
|
||
for r := 0; r < 5; r++ {
|
||
for c := 0; c < 5; c++ {
|
||
if g.Board[r][c].IsMine {
|
||
g.Reveal(msp(r, c))
|
||
break
|
||
}
|
||
}
|
||
if g.Result != nil {
|
||
break
|
||
}
|
||
}
|
||
if g.Result == nil {
|
||
t.Fatalf("партия должна была завершиться поражением")
|
||
}
|
||
if err := g.Reveal(msp(0, 0)); err != ErrMinesweeperGameOver {
|
||
t.Errorf("ожидалась ErrMinesweeperGameOver, получено: %v", err)
|
||
}
|
||
if err := g.ToggleFlag(msp(0, 0)); err != ErrMinesweeperGameOver {
|
||
t.Errorf("ожидалась ErrMinesweeperGameOver при флаге, получено: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestChordOpensNeighborsWhenFlagsMatch(t *testing.T) {
|
||
g := NewMinesweeperGame(5, 5, 2)
|
||
g.minesPlaced = true
|
||
g.Board[0][1].IsMine = true
|
||
g.Board[1][0].IsMine = true
|
||
for r := 0; r < 5; r++ {
|
||
for c := 0; c < 5; c++ {
|
||
if g.Board[r][c].IsMine {
|
||
continue
|
||
}
|
||
n := 0
|
||
for _, nb := range msNeighbors(msp(r, c)) {
|
||
if g.inBounds(nb) && g.Board[nb.Row][nb.Col].IsMine {
|
||
n++
|
||
}
|
||
}
|
||
g.Board[r][c].AdjacentMines = n
|
||
}
|
||
}
|
||
g.Board[0][0].Revealed = true
|
||
g.RevealedCount++
|
||
|
||
g.ToggleFlag(msp(0, 1))
|
||
g.ToggleFlag(msp(1, 0))
|
||
|
||
if err := g.Chord(msp(0, 0)); err != nil {
|
||
t.Fatalf("неожиданная ошибка хорды: %v", err)
|
||
}
|
||
if !g.Board[1][1].Revealed {
|
||
t.Errorf("хорда должна была открыть оставшегося непомеченного соседа (1,1)")
|
||
}
|
||
}
|
||
|
||
func TestChordDoesNothingWhenFlagsDontMatch(t *testing.T) {
|
||
g := NewMinesweeperGame(5, 5, 2)
|
||
g.minesPlaced = true
|
||
g.Board[0][1].IsMine = true
|
||
g.Board[1][0].IsMine = true
|
||
for r := 0; r < 5; r++ {
|
||
for c := 0; c < 5; c++ {
|
||
if g.Board[r][c].IsMine {
|
||
continue
|
||
}
|
||
n := 0
|
||
for _, nb := range msNeighbors(msp(r, c)) {
|
||
if g.inBounds(nb) && g.Board[nb.Row][nb.Col].IsMine {
|
||
n++
|
||
}
|
||
}
|
||
g.Board[r][c].AdjacentMines = n
|
||
}
|
||
}
|
||
g.Board[0][0].Revealed = true
|
||
g.RevealedCount++
|
||
// флаг только один, а мин рядом две — хорда не должна сработать
|
||
g.ToggleFlag(msp(0, 1))
|
||
|
||
if err := g.Chord(msp(0, 0)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if g.Board[1][1].Revealed {
|
||
t.Errorf("хорда не должна была ничего открыть при несовпадении числа флагов")
|
||
}
|
||
}
|
||
|
||
func TestChordRequiresRevealedCell(t *testing.T) {
|
||
g := NewMinesweeperGame(5, 5, 2)
|
||
g.minesPlaced = true
|
||
if err := g.Chord(msp(0, 0)); err != ErrMinesweeperNotRevealed {
|
||
t.Errorf("ожидалась ErrMinesweeperNotRevealed, получено: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestChordCanTriggerLoss(t *testing.T) {
|
||
g := NewMinesweeperGame(5, 5, 1)
|
||
g.minesPlaced = true
|
||
g.Board[0][1].IsMine = true
|
||
for r := 0; r < 5; r++ {
|
||
for c := 0; c < 5; c++ {
|
||
if g.Board[r][c].IsMine {
|
||
continue
|
||
}
|
||
n := 0
|
||
for _, nb := range msNeighbors(msp(r, c)) {
|
||
if g.inBounds(nb) && g.Board[nb.Row][nb.Col].IsMine {
|
||
n++
|
||
}
|
||
}
|
||
g.Board[r][c].AdjacentMines = n
|
||
}
|
||
}
|
||
g.Board[0][0].Revealed = true
|
||
g.RevealedCount++
|
||
// неверно ставим флаг НЕ на мину, а число соседних мин у (0,0)
|
||
// равно 1 — хорда решит, что "мина учтена", и откроет саму мину
|
||
g.ToggleFlag(msp(1, 1))
|
||
|
||
if err := g.Chord(msp(0, 0)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if g.Result == nil || g.Result.Won {
|
||
t.Fatalf("неверно поставленный флаг при хорде должен был привести к проигрышу")
|
||
}
|
||
}
|