package main import "errors" // ПРИМЕЧАНИЕ О ПРАВИЛАХ: реализованы полные правила шахмат, включая // рокировку (в обе стороны, со всеми условиями: ни король, ни ладья // не должны были ходить, поля между ними свободны, король не под // шахом и не проходит через атакованное поле) и взятие на проходе. // Превращение пешки УПРОЩЕНО: пешка всегда превращается в ферзя // (автоматически, без выбора фигуры) — это стандартное упрощение // для большинства цифровых реализаций, так как подавляющее // большинство реальных превращений и так выбирают ферзя. Из // официальных правил ничьей реализованы мат/пат, недостаточность // материала и упрощённый аналог правила 75 полуходов без взятия и // хода пешкой (автоматическое завершение партии, без необходимости // заявлять ничью); троекратное повторение позиции НЕ реализовано // (потребовало бы хранить историю всех позиций). // ChessColor — цвет стороны. type ChessColor int const ( ChessWhite ChessColor = iota ChessBlack ) func (c ChessColor) Opponent() ChessColor { if c == ChessWhite { return ChessBlack } return ChessWhite } // ChessPieceType — тип фигуры. type ChessPieceType int const ( ChessPawn ChessPieceType = iota ChessKnight ChessBishop ChessRook ChessQueen ChessKing ) // ChessPiece — фигура на доске. type ChessPiece struct { Color ChessColor Type ChessPieceType } // ChessPos — координаты клетки (0-7, 0-7). Row 0 = 8-я горизонталь // (чёрные сзади), Row 7 = 1-я горизонталь (белые сзади). Col 0-7 = // вертикали a-h. type ChessPos struct { Row, Col int } func (p ChessPos) inBounds() bool { return p.Row >= 0 && p.Row < 8 && p.Col >= 0 && p.Col < 8 } var ( ErrChessNoPiece = errors.New("на этой клетке нет вашей фигуры") ErrChessInvalidMove = errors.New("такой ход недопустим") ErrChessGameOver = errors.New("партия уже завершена") ) // chessMoveOption — один вариант хода для конкретной фигуры. type chessMoveOption struct { To ChessPos IsEnPassant bool IsCastleKingside bool IsCastleQueenside bool IsPromotion bool } // ChessResult — итог завершившейся партии. type ChessResult struct { Winner ChessColor Checkmate bool Draw bool DrawReason string // "stalemate", "insufficient_material", "no_progress" } // maxChessHalfmovesWithoutProgress — защита от бесконечных партий // без взятий и ходов пешками (упрощённый аналог официального // правила 75 полуходов). const maxChessHalfmovesWithoutProgress = 150 // ChessGameState — полное состояние партии в шахматы. type ChessGameState struct { Board [8][8]*ChessPiece Turn ChessColor WhiteKingMoved bool WhiteRookQueensideMoved bool // ладья a1 WhiteRookKingsideMoved bool // ладья h1 BlackKingMoved bool BlackRookQueensideMoved bool // ладья a8 BlackRookKingsideMoved bool // ладья h8 EnPassantTarget *ChessPos // клетка "за" пешкой, только что шагнувшей на 2 — цель взятия на проходе HalfmoveClock int // полуходов без взятия/хода пешкой Result *ChessResult } // NewChessGame создаёт новую партию со стандартной расстановкой; // первый ход — за белыми. func NewChessGame() *ChessGameState { g := &ChessGameState{Turn: ChessWhite} backRank := []ChessPieceType{ChessRook, ChessKnight, ChessBishop, ChessQueen, ChessKing, ChessBishop, ChessKnight, ChessRook} for col, t := range backRank { g.Board[0][col] = &ChessPiece{Color: ChessBlack, Type: t} g.Board[7][col] = &ChessPiece{Color: ChessWhite, Type: t} } for col := 0; col < 8; col++ { g.Board[1][col] = &ChessPiece{Color: ChessBlack, Type: ChessPawn} g.Board[6][col] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} } return g } func (p ChessPos) add(dr, dc int) ChessPos { return ChessPos{Row: p.Row + dr, Col: p.Col + dc} } var chessBishopDirs = []ChessPos{{Row: -1, Col: -1}, {Row: -1, Col: 1}, {Row: 1, Col: -1}, {Row: 1, Col: 1}} var chessRookDirs = []ChessPos{{Row: -1, Col: 0}, {Row: 1, Col: 0}, {Row: 0, Col: -1}, {Row: 0, Col: 1}} var chessKnightOffsets = []ChessPos{{-2, -1}, {-2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}, {2, -1}, {2, 1}} // slidingMoves перечисляет ходы скользящей фигуры (ладья/слон/ферзь) // вдоль набора направлений, останавливаясь на первой фигуре (со // взятием, если это вражеская). func (g *ChessGameState) slidingMoves(pos ChessPos, dirs []ChessPos, color ChessColor) []chessMoveOption { var out []chessMoveOption for _, d := range dirs { for step := 1; ; step++ { np := pos.add(d.Row*step, d.Col*step) if !np.inBounds() { break } occupant := g.Board[np.Row][np.Col] if occupant == nil { out = append(out, chessMoveOption{To: np}) continue } if occupant.Color != color { out = append(out, chessMoveOption{To: np}) } break } } return out } // pseudoLegalMoves перечисляет ходы фигуры без учёта того, остаётся // ли после них свой король под шахом (эта проверка — уровнем выше, // в LegalMovesFrom). func (g *ChessGameState) pseudoLegalMoves(pos ChessPos) []chessMoveOption { piece := g.Board[pos.Row][pos.Col] if piece == nil { return nil } switch piece.Type { case ChessKnight: var out []chessMoveOption for _, o := range chessKnightOffsets { np := pos.add(o.Row, o.Col) if !np.inBounds() { continue } occupant := g.Board[np.Row][np.Col] if occupant == nil || occupant.Color != piece.Color { out = append(out, chessMoveOption{To: np}) } } return out case ChessBishop: return g.slidingMoves(pos, chessBishopDirs, piece.Color) case ChessRook: return g.slidingMoves(pos, chessRookDirs, piece.Color) case ChessQueen: out := g.slidingMoves(pos, chessBishopDirs, piece.Color) return append(out, g.slidingMoves(pos, chessRookDirs, piece.Color)...) case ChessKing: return g.kingMoves(pos, piece.Color) case ChessPawn: return g.pawnMoves(pos, piece.Color) } return nil } func (g *ChessGameState) kingMoves(pos ChessPos, color ChessColor) []chessMoveOption { var out []chessMoveOption for _, d := range append(append([]ChessPos{}, chessBishopDirs...), chessRookDirs...) { np := pos.add(d.Row, d.Col) if !np.inBounds() { continue } occupant := g.Board[np.Row][np.Col] if occupant == nil || occupant.Color != color { out = append(out, chessMoveOption{To: np}) } } out = append(out, g.castlingMoves(pos, color)...) return out } func (g *ChessGameState) castlingMoves(kingPos ChessPos, color ChessColor) []chessMoveOption { if g.inCheck(color) { return nil // нельзя рокироваться, находясь под шахом } row := kingPos.Row var kingMoved, rookQMoved, rookKMoved bool if color == ChessWhite { kingMoved, rookQMoved, rookKMoved = g.WhiteKingMoved, g.WhiteRookQueensideMoved, g.WhiteRookKingsideMoved } else { kingMoved, rookQMoved, rookKMoved = g.BlackKingMoved, g.BlackRookQueensideMoved, g.BlackRookKingsideMoved } if kingMoved { return nil } var out []chessMoveOption enemy := color.Opponent() // короткая рокировка (kingside): ладья h, король e->g if !rookKMoved && g.Board[row][5] == nil && g.Board[row][6] == nil { if rook := g.Board[row][7]; rook != nil && rook.Type == ChessRook && rook.Color == color { if !g.isSquareAttacked(ChessPos{row, 5}, enemy) && !g.isSquareAttacked(ChessPos{row, 6}, enemy) { out = append(out, chessMoveOption{To: ChessPos{row, 6}, IsCastleKingside: true}) } } } // длинная рокировка (queenside): ладья a, король e->c if !rookQMoved && g.Board[row][1] == nil && g.Board[row][2] == nil && g.Board[row][3] == nil { if rook := g.Board[row][0]; rook != nil && rook.Type == ChessRook && rook.Color == color { if !g.isSquareAttacked(ChessPos{row, 3}, enemy) && !g.isSquareAttacked(ChessPos{row, 2}, enemy) { out = append(out, chessMoveOption{To: ChessPos{row, 2}, IsCastleQueenside: true}) } } } return out } func (g *ChessGameState) pawnMoves(pos ChessPos, color ChessColor) []chessMoveOption { var out []chessMoveOption forward := 1 startRow := 1 lastRow := 7 if color == ChessWhite { forward = -1 startRow = 6 lastRow = 0 } one := pos.add(forward, 0) if one.inBounds() && g.Board[one.Row][one.Col] == nil { out = append(out, chessMoveOption{To: one, IsPromotion: one.Row == lastRow}) if pos.Row == startRow { two := pos.add(forward*2, 0) if g.Board[two.Row][two.Col] == nil { out = append(out, chessMoveOption{To: two}) } } } for _, dc := range []int{-1, 1} { capture := pos.add(forward, dc) if !capture.inBounds() { continue } if occupant := g.Board[capture.Row][capture.Col]; occupant != nil && occupant.Color != color { out = append(out, chessMoveOption{To: capture, IsPromotion: capture.Row == lastRow}) continue } if g.EnPassantTarget != nil && *g.EnPassantTarget == capture { out = append(out, chessMoveOption{To: capture, IsEnPassant: true}) } } return out } // isSquareAttacked проверяет, атакована ли клетка pos какой-либо // фигурой цвета byColor. func (g *ChessGameState) isSquareAttacked(pos ChessPos, byColor ChessColor) bool { dir := 1 if byColor == ChessWhite { dir = -1 } for _, dc := range []int{-1, 1} { p := pos.add(-dir, dc) if p.inBounds() { if occ := g.Board[p.Row][p.Col]; occ != nil && occ.Color == byColor && occ.Type == ChessPawn { return true } } } for _, o := range chessKnightOffsets { p := pos.add(o.Row, o.Col) if p.inBounds() { if occ := g.Board[p.Row][p.Col]; occ != nil && occ.Color == byColor && occ.Type == ChessKnight { return true } } } for _, d := range append(append([]ChessPos{}, chessBishopDirs...), chessRookDirs...) { p := pos.add(d.Row, d.Col) if p.inBounds() { if occ := g.Board[p.Row][p.Col]; occ != nil && occ.Color == byColor && occ.Type == ChessKing { return true } } } for _, d := range chessBishopDirs { for step := 1; ; step++ { p := pos.add(d.Row*step, d.Col*step) if !p.inBounds() { break } occ := g.Board[p.Row][p.Col] if occ == nil { continue } if occ.Color == byColor && (occ.Type == ChessBishop || occ.Type == ChessQueen) { return true } break } } for _, d := range chessRookDirs { for step := 1; ; step++ { p := pos.add(d.Row*step, d.Col*step) if !p.inBounds() { break } occ := g.Board[p.Row][p.Col] if occ == nil { continue } if occ.Color == byColor && (occ.Type == ChessRook || occ.Type == ChessQueen) { return true } break } } return false } func (g *ChessGameState) findKing(color ChessColor) ChessPos { for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { if p := g.Board[r][c]; p != nil && p.Color == color && p.Type == ChessKing { return ChessPos{r, c} } } } return ChessPos{-1, -1} // не должно происходить при корректном состоянии } func (g *ChessGameState) inCheck(color ChessColor) bool { return g.isSquareAttacked(g.findKing(color), color.Opponent()) } // cloneChess создаёт независимую глубокую копию — нужна для // проверки "не остаётся ли король под шахом после хода" и для // минимакса бота. func cloneChess(g *ChessGameState) *ChessGameState { clone := *g for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { if p := g.Board[r][c]; p != nil { cp := *p clone.Board[r][c] = &cp } else { clone.Board[r][c] = nil } } } if g.EnPassantTarget != nil { ep := *g.EnPassantTarget clone.EnPassantTarget = &ep } if g.Result != nil { res := *g.Result clone.Result = &res } return &clone } // LegalMovesFrom возвращает допустимые ходы фигуры в pos — // псевдолегальные ходы, отфильтрованные так, чтобы после них // собственный король не оставался под шахом. func (g *ChessGameState) LegalMovesFrom(pos ChessPos) []chessMoveOption { if g.Result != nil { return nil } piece := g.Board[pos.Row][pos.Col] if piece == nil || piece.Color != g.Turn { return nil } var out []chessMoveOption for _, opt := range g.pseudoLegalMoves(pos) { clone := cloneChess(g) clone.applyMove(pos, opt) if !clone.inCheck(piece.Color) { out = append(out, opt) } } return out } // applyMove выполняет механику хода (перемещение фигур, взятие на // проходе, рокировка, превращение) БЕЗ проверки легальности — // вызывающий код должен был её уже провести. func (g *ChessGameState) applyMove(from ChessPos, opt chessMoveOption) { piece := g.Board[from.Row][from.Col] if opt.IsEnPassant { g.Board[from.Row][opt.To.Col] = nil // взятая пешка стоит на строке from, столбце назначения } if opt.IsCastleKingside { rook := g.Board[from.Row][7] g.Board[from.Row][7] = nil g.Board[from.Row][5] = rook } if opt.IsCastleQueenside { rook := g.Board[from.Row][0] g.Board[from.Row][0] = nil g.Board[from.Row][3] = rook } g.Board[from.Row][from.Col] = nil if opt.IsPromotion { g.Board[opt.To.Row][opt.To.Col] = &ChessPiece{Color: piece.Color, Type: ChessQueen} } else { g.Board[opt.To.Row][opt.To.Col] = piece } if piece.Type == ChessKing { if piece.Color == ChessWhite { g.WhiteKingMoved = true } else { g.BlackKingMoved = true } } markRookMoved := func(pos ChessPos) { if pos.Row == 7 && pos.Col == 0 { g.WhiteRookQueensideMoved = true } if pos.Row == 7 && pos.Col == 7 { g.WhiteRookKingsideMoved = true } if pos.Row == 0 && pos.Col == 0 { g.BlackRookQueensideMoved = true } if pos.Row == 0 && pos.Col == 7 { g.BlackRookKingsideMoved = true } } markRookMoved(from) markRookMoved(opt.To) // на случай взятия ладьи соперника if piece.Type == ChessPawn && abs(opt.To.Row-from.Row) == 2 { mid := ChessPos{Row: (from.Row + opt.To.Row) / 2, Col: from.Col} g.EnPassantTarget = &mid } else { g.EnPassantTarget = nil } } func abs(n int) int { if n < 0 { return -n } return n } // hasAnyLegalMove проверяет, есть ли у стороны color хоть один // легальный ход где-либо на доске. func (g *ChessGameState) hasAnyLegalMove(color ChessColor) bool { for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { p := g.Board[r][c] if p == nil || p.Color != color { continue } if len(g.LegalMovesFrom(ChessPos{r, c})) > 0 { return true } } } return false } // hasInsufficientMaterial проверяет самые распространённые // теоретически недостаточные для мата комбинации: King vs King, // King+Knight vs King, King+Bishop vs King. Более редкие ничейные // комбинации (например, слоны на клетках одного цвета с обеих // сторон) не проверяются — упрощение. func (g *ChessGameState) hasInsufficientMaterial() bool { var minor []ChessPieceType for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { p := g.Board[r][c] if p == nil || p.Type == ChessKing { continue } if p.Type != ChessKnight && p.Type != ChessBishop { return false } minor = append(minor, p.Type) } } return len(minor) <= 1 } // Move выполняет ход фигурой из from в to, если он допустим по // текущим правилам (см. LegalMovesFrom). Превращение пешки всегда // автоматически происходит в ферзя. func (g *ChessGameState) Move(from, to ChessPos) error { if g.Result != nil { return ErrChessGameOver } piece := g.Board[from.Row][from.Col] if piece == nil || piece.Color != g.Turn { return ErrChessNoPiece } options := g.LegalMovesFrom(from) var chosen *chessMoveOption for i := range options { if options[i].To == to { chosen = &options[i] break } } if chosen == nil { return ErrChessInvalidMove } isCaptureOrPawn := piece.Type == ChessPawn || g.Board[to.Row][to.Col] != nil || chosen.IsEnPassant g.applyMove(from, *chosen) if isCaptureOrPawn { g.HalfmoveClock = 0 } else { g.HalfmoveClock++ } g.Turn = g.Turn.Opponent() g.checkGameOver() return nil } func (g *ChessGameState) checkGameOver() { if g.hasInsufficientMaterial() { g.Result = &ChessResult{Draw: true, DrawReason: "insufficient_material"} return } if g.HalfmoveClock >= maxChessHalfmovesWithoutProgress { g.Result = &ChessResult{Draw: true, DrawReason: "no_progress"} return } if !g.hasAnyLegalMove(g.Turn) { if g.inCheck(g.Turn) { g.Result = &ChessResult{Winner: g.Turn.Opponent(), Checkmate: true} } else { g.Result = &ChessResult{Draw: true, DrawReason: "stalemate"} } } }