go_games_collection/chess_game.go

569 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"}
}
}
}