go_games_collection/minesweeper_game_test.go

326 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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("неверно поставленный флаг при хорде должен был привести к проигрышу")
}
}