300 lines
9 KiB
Go
300 lines
9 KiB
Go
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
|
||
}
|