package main import "errors" // ПРИМЕЧАНИЕ О ПРАВИЛАХ: реализованы базовые правила Го — свободы // групп камней, взятие групп без свобод, запрет самоубийственного // хода (кроме случая, когда ход сам берёт вражеские камни и тем // самым получает свободу), простое правило ко (нельзя немедленно // воссоздать позицию, существовавшую до хода соперника). Подсчёт // очков — по КИТАЙСКИМ (площадным) правилам: очки = количество // своих камней на доске + количество пустых точек, граничащих // ИСКЛЮЧИТЕЛЬНО с этим цветом (нейтральные точки, граничащие с // обоими цветами, не засчитываются никому). Белые получают коми // (компенсацию за то, что чёрные ходят первыми) — 7.5 очка на // любом размере доски (в реальности коми иногда варьируется в // зависимости от размера доски и разновидности правил — здесь для // простоты используется одно фиксированное значение). // // СУЩЕСТВЕННОЕ УПРОЩЕНИЕ: фаза "удаления мёртвых камней" перед // подсчётом очков НЕ реализована. После двух пасов подряд партия // сразу же оценивается по камням, буквально оставшимся на доске — // если камень технически окружён, но не взят взаправду, он // засчитывается как живой своему цвету. Это значит, что для // корректного счёта игроки должны реально ВЗЯТЬ мёртвые камни // соперника до двух пасов, а не полагаться на то, что они "и так // мертвы". Тройное повторение позиции (супер-ко) не реализовано — // только простое ко (одна запрещённая точка на один ход вперёд). // GoColor — цвет камня. GoEmpty используется как значение пустой // точки на доске. type GoColor int const ( GoEmpty GoColor = iota GoBlack GoWhite ) func (c GoColor) Opponent() GoColor { switch c { case GoBlack: return GoWhite case GoWhite: return GoBlack } return GoEmpty } // GoPos — координаты точки пересечения (0-based, row/col). type GoPos struct { Row, Col int } // GoKomi — компенсация очков белым за то, что чёрные ходят первыми. const GoKomi = 7.5 var ( ErrGoOutOfBounds = errors.New("точка вне пределов доски") ErrGoOccupied = errors.New("точка уже занята") ErrGoSuicide = errors.New("этот ход самоубийственный — так ходить нельзя") ErrGoKoViolation = errors.New("этот ход запрещён правилом ко") ErrGoGameOver = errors.New("партия уже завершена") ) // GoResult — итог завершившейся партии. type GoResult struct { BlackScore float64 WhiteScore float64 Winner GoColor } // GoGameState — полное состояние партии в Го. type GoGameState struct { Size int Board [][]GoColor // Board[row][col] Turn GoColor Passes int // подряд идущих пасов (0, 1 или 2 — на 2 партия завершается) CapturedByBlack int // сколько камней взяли чёрные (для отображения, не влияет на площадной счёт) CapturedByWhite int prevBoardBeforeOpponentMove [][]GoColor // снимок доски до хода соперника — для правила ко Result *GoResult } // NewGoGame создаёт новую пустую партию на доске size x size (обычно // 9, 13 или 19). Первый ход — за чёрными. func NewGoGame(size int) *GoGameState { board := make([][]GoColor, size) for r := range board { board[r] = make([]GoColor, size) } return &GoGameState{Size: size, Board: board, Turn: GoBlack} } func (g *GoGameState) inBounds(p GoPos) bool { return p.Row >= 0 && p.Row < g.Size && p.Col >= 0 && p.Col < g.Size } func goNeighbors(p GoPos) [4]GoPos { return [4]GoPos{{p.Row - 1, p.Col}, {p.Row + 1, p.Col}, {p.Row, p.Col - 1}, {p.Row, p.Col + 1}} } func cloneGoBoard(b [][]GoColor) [][]GoColor { clone := make([][]GoColor, len(b)) for i, row := range b { clone[i] = append([]GoColor{}, row...) } return clone } func boardsEqual(a, b [][]GoColor) bool { if a == nil || b == nil { return a == nil && b == nil } if len(a) != len(b) { return false } for i := range a { if len(a[i]) != len(b[i]) { return false } for j := range a[i] { if a[i][j] != b[i][j] { return false } } } return true } // groupAndLiberties возвращает все точки связной группы одного // цвета, содержащей pos, и число различных пустых точек-свобод, // граничащих с этой группой. func (g *GoGameState) groupAndLiberties(pos GoPos) ([]GoPos, int) { color := g.Board[pos.Row][pos.Col] visited := map[GoPos]bool{pos: true} liberties := map[GoPos]bool{} queue := []GoPos{pos} group := []GoPos{pos} for len(queue) > 0 { cur := queue[0] queue = queue[1:] for _, n := range goNeighbors(cur) { if !g.inBounds(n) { continue } switch g.Board[n.Row][n.Col] { case GoEmpty: liberties[n] = true case color: if !visited[n] { visited[n] = true group = append(group, n) queue = append(queue, n) } } } } return group, len(liberties) } // removeGroup убирает с доски все камни группы (взятие). func (g *GoGameState) removeGroup(group []GoPos) { for _, p := range group { g.Board[p.Row][p.Col] = GoEmpty } } // Play делает ход текущего игрока в точку pos. func (g *GoGameState) Play(pos GoPos) error { if g.Result != nil { return ErrGoGameOver } if !g.inBounds(pos) { return ErrGoOutOfBounds } if g.Board[pos.Row][pos.Col] != GoEmpty { return ErrGoOccupied } color := g.Turn boardBeforeThisMove := cloneGoBoard(g.Board) g.Board[pos.Row][pos.Col] = color captured := 0 for _, n := range goNeighbors(pos) { if !g.inBounds(n) || g.Board[n.Row][n.Col] != color.Opponent() { continue } group, libs := g.groupAndLiberties(n) if libs == 0 { g.removeGroup(group) captured += len(group) } } _, ownLiberties := g.groupAndLiberties(pos) if ownLiberties == 0 { g.Board = boardBeforeThisMove // откат: самоубийственный ход return ErrGoSuicide } if boardsEqual(g.Board, g.prevBoardBeforeOpponentMove) { g.Board = boardBeforeThisMove // откат: нарушение правила ко return ErrGoKoViolation } if color == GoBlack { g.CapturedByBlack += captured } else { g.CapturedByWhite += captured } g.prevBoardBeforeOpponentMove = boardBeforeThisMove g.Passes = 0 g.Turn = color.Opponent() return nil } // Pass — текущий игрок пасует. Два паса подряд завершают партию. func (g *GoGameState) Pass() error { if g.Result != nil { return ErrGoGameOver } g.Passes++ g.prevBoardBeforeOpponentMove = cloneGoBoard(g.Board) g.Turn = g.Turn.Opponent() if g.Passes >= 2 { g.finishGame() } return nil } // IsLegal проверяет, допустим ли ход в pos для текущего игрока, не // изменяя реальное состояние партии (использует пробный ход на // копии доски). func (g *GoGameState) IsLegal(pos GoPos) bool { if g.Result != nil || !g.inBounds(pos) || g.Board[pos.Row][pos.Col] != GoEmpty { return false } sim := &GoGameState{ Size: g.Size, Board: cloneGoBoard(g.Board), Turn: g.Turn, prevBoardBeforeOpponentMove: g.prevBoardBeforeOpponentMove, } return sim.Play(pos) == nil } // finishGame считает итоговый счёт по площадным (китайским) правилам // и определяет победителя. func (g *GoGameState) finishGame() { blackScore, whiteScore := 0, 0 for r := 0; r < g.Size; r++ { for c := 0; c < g.Size; c++ { switch g.Board[r][c] { case GoBlack: blackScore++ case GoWhite: whiteScore++ } } } visited := make([][]bool, g.Size) for i := range visited { visited[i] = make([]bool, g.Size) } for r := 0; r < g.Size; r++ { for c := 0; c < g.Size; c++ { if visited[r][c] || g.Board[r][c] != GoEmpty { continue } region, touchesBlack, touchesWhite := g.floodEmptyRegion(GoPos{r, c}, visited) switch { case touchesBlack && !touchesWhite: blackScore += len(region) case touchesWhite && !touchesBlack: whiteScore += len(region) } } } result := &GoResult{BlackScore: float64(blackScore), WhiteScore: float64(whiteScore) + GoKomi} if result.BlackScore > result.WhiteScore { result.Winner = GoBlack } else { result.Winner = GoWhite } g.Result = result } // floodEmptyRegion обходит связную область пустых точек, начиная с // start, и определяет, граничит ли она с чёрными и/или белыми // камнями. func (g *GoGameState) floodEmptyRegion(start GoPos, visited [][]bool) (region []GoPos, touchesBlack, touchesWhite bool) { queue := []GoPos{start} visited[start.Row][start.Col] = true region = append(region, start) for len(queue) > 0 { cur := queue[0] queue = queue[1:] for _, n := range goNeighbors(cur) { if !g.inBounds(n) { continue } switch g.Board[n.Row][n.Col] { case GoEmpty: if !visited[n.Row][n.Col] { visited[n.Row][n.Col] = true region = append(region, n) queue = append(queue, n) } case GoBlack: touchesBlack = true case GoWhite: touchesWhite = true } } } return region, touchesBlack, touchesWhite }