569 lines
18 KiB
Go
569 lines
18 KiB
Go
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"}
|
||
}
|
||
}
|
||
}
|