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