package main import ( "errors" "math/rand" "time" ) // ПРИМЕЧАНИЕ О ПРАВИЛАХ: классический Сапёр. Мины расставляются // только ПОСЛЕ первого клика — так, чтобы кликнутая клетка и все её // соседи гарантированно не содержали мину (это даёт более приятное, // обычно достаточно большое первое открытие, а не голую цифру) — // распространённое улучшение поверх самой базовой гарантии "хотя бы // кликнутая клетка не мина". "Аккорд" (быстрое открытие соседей у // уже открытой цифры, если рядом расставлено ровно нужное число // флагов) реализован, как и в привычных версиях игры. var ( ErrMinesweeperGameOver = errors.New("партия уже завершена") ErrMinesweeperOutOfBounds = errors.New("клетка вне пределов поля") ErrMinesweeperFlagged = errors.New("сначала снимите флаг с этой клетки") ErrMinesweeperAlreadyOpen = errors.New("клетка уже открыта") ErrMinesweeperNotRevealed = errors.New("эта клетка ещё не открыта") ) // MinesweeperPos — координаты клетки (0-based, row/col). type MinesweeperPos struct { Row, Col int } // minesweeperCell — состояние одной клетки поля. type minesweeperCell struct { Revealed bool Flagged bool IsMine bool AdjacentMines int } // MinesweeperResult — итог завершившейся партии. type MinesweeperResult struct { Won bool } // MinesweeperGameState — полное состояние партии в Сапёра. type MinesweeperGameState struct { Width, Height int MineCount int Board [][]minesweeperCell minesPlaced bool // мины расставляются только после первого клика RevealedCount int FlagCount int Result *MinesweeperResult rng *rand.Rand } // NewMinesweeperGame создаёт новое поле width x height с mineCount // мин (не больше width*height-1 — хотя бы одна клетка гарантированно // останется небезопасной для мины, чтобы было куда сходить первым // ходом). Сами мины появятся только после первого хода — см. Reveal. func NewMinesweeperGame(width, height, mineCount int) *MinesweeperGameState { maxMines := width*height - 1 if mineCount > maxMines { mineCount = maxMines } g := &MinesweeperGameState{ Width: width, Height: height, MineCount: mineCount, rng: rand.New(rand.NewSource(time.Now().UnixNano())), } g.Board = make([][]minesweeperCell, height) for r := range g.Board { g.Board[r] = make([]minesweeperCell, width) } return g } func (g *MinesweeperGameState) inBounds(p MinesweeperPos) bool { return p.Row >= 0 && p.Row < g.Height && p.Col >= 0 && p.Col < g.Width } func msNeighbors(p MinesweeperPos) []MinesweeperPos { out := make([]MinesweeperPos, 0, 8) for dr := -1; dr <= 1; dr++ { for dc := -1; dc <= 1; dc++ { if dr == 0 && dc == 0 { continue } out = append(out, MinesweeperPos{Row: p.Row + dr, Col: p.Col + dc}) } } return out } // placeMines расставляет мины случайно (используя переданный // генератор rnd — так тесты могут вызывать её напрямую // детерминированно), избегая точки first и всех её соседей // (безопасная зона первого клика), затем считает число мин-соседей // для каждой клетки. func (g *MinesweeperGameState) placeMines(first MinesweeperPos, rnd *rand.Rand) { safe := map[MinesweeperPos]bool{first: true} for _, n := range msNeighbors(first) { if g.inBounds(n) { safe[n] = true } } var candidates []MinesweeperPos for r := 0; r < g.Height; r++ { for c := 0; c < g.Width; c++ { p := MinesweeperPos{Row: r, Col: c} if !safe[p] { candidates = append(candidates, p) } } } count := g.MineCount if count > len(candidates) { count = len(candidates) // защитный случай — слишком мало свободных клеток } rnd.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) for i := 0; i < count; i++ { p := candidates[i] g.Board[p.Row][p.Col].IsMine = true } for r := 0; r < g.Height; r++ { for c := 0; c < g.Width; c++ { p := MinesweeperPos{Row: r, Col: c} n := 0 for _, adj := range msNeighbors(p) { if g.inBounds(adj) && g.Board[adj.Row][adj.Col].IsMine { n++ } } g.Board[r][c].AdjacentMines = n } } g.minesPlaced = true } // Reveal открывает клетку pos. Если это первый ход партии, сначала // расставляет мины (см. placeMines). func (g *MinesweeperGameState) Reveal(pos MinesweeperPos) error { if g.Result != nil { return ErrMinesweeperGameOver } if !g.inBounds(pos) { return ErrMinesweeperOutOfBounds } if !g.minesPlaced { g.placeMines(pos, g.rng) } cell := &g.Board[pos.Row][pos.Col] if cell.Flagged { return ErrMinesweeperFlagged } if cell.Revealed { return ErrMinesweeperAlreadyOpen } if cell.IsMine { g.revealAllMines() g.Result = &MinesweeperResult{Won: false} return nil } g.floodReveal(pos) g.checkWin() return nil } // floodReveal открывает pos и, если у неё 0 мин-соседей, рекурсивно // (через очередь) открывает всех соседей — классическое каскадное // открытие пустых областей. func (g *MinesweeperGameState) floodReveal(start MinesweeperPos) { if g.Board[start.Row][start.Col].Revealed || g.Board[start.Row][start.Col].Flagged { return } queue := []MinesweeperPos{start} g.Board[start.Row][start.Col].Revealed = true g.RevealedCount++ for len(queue) > 0 { cur := queue[0] queue = queue[1:] if g.Board[cur.Row][cur.Col].AdjacentMines != 0 { continue } for _, n := range msNeighbors(cur) { if !g.inBounds(n) { continue } nc := &g.Board[n.Row][n.Col] if nc.Revealed || nc.Flagged || nc.IsMine { continue } nc.Revealed = true g.RevealedCount++ queue = append(queue, n) } } } // Chord — быстрое открытие соседей уже открытой цифры, если число // флагов вокруг неё совпадает с самой цифрой (вызывается на уже // открытой клетке). func (g *MinesweeperGameState) Chord(pos MinesweeperPos) error { if g.Result != nil { return ErrMinesweeperGameOver } if !g.inBounds(pos) { return ErrMinesweeperOutOfBounds } cell := g.Board[pos.Row][pos.Col] if !cell.Revealed { return ErrMinesweeperNotRevealed } flagCount := 0 for _, n := range msNeighbors(pos) { if g.inBounds(n) && g.Board[n.Row][n.Col].Flagged { flagCount++ } } if flagCount != cell.AdjacentMines { return nil // недостаточно/слишком много флагов — аккорд не срабатывает, это не ошибка } for _, n := range msNeighbors(pos) { if !g.inBounds(n) { continue } nc := g.Board[n.Row][n.Col] if nc.Revealed || nc.Flagged { continue } if nc.IsMine { g.revealAllMines() g.Result = &MinesweeperResult{Won: false} return nil } g.floodReveal(n) } if g.Result == nil { g.checkWin() } return nil } func (g *MinesweeperGameState) revealAllMines() { for r := 0; r < g.Height; r++ { for c := 0; c < g.Width; c++ { if g.Board[r][c].IsMine { g.Board[r][c].Revealed = true } } } } func (g *MinesweeperGameState) checkWin() { total := g.Width * g.Height if g.RevealedCount == total-g.MineCount { g.Result = &MinesweeperResult{Won: true} } } // ToggleFlag ставит/снимает флаг на скрытой клетке. func (g *MinesweeperGameState) ToggleFlag(pos MinesweeperPos) error { if g.Result != nil { return ErrMinesweeperGameOver } if !g.inBounds(pos) { return ErrMinesweeperOutOfBounds } cell := &g.Board[pos.Row][pos.Col] if cell.Revealed { return ErrMinesweeperAlreadyOpen } if cell.Flagged { cell.Flagged = false g.FlagCount-- } else { cell.Flagged = true g.FlagCount++ } return nil } // MinesRemaining — сколько мин, по мнению счётчика, ещё не // помечено флагом (может уйти в минус при перефлаживании — это // нормально и так же ведёт себя в привычных версиях игры). func (g *MinesweeperGameState) MinesRemaining() int { return g.MineCount - g.FlagCount }