package main import "errors" // ПРИМЕЧАНИЕ О ПРАВИЛАХ: реализованы русские шашки (не международные // и не английские): доска 8х8, взятие обязательно (но правило // "бить максимально" НЕ действует — подходит любое доступное взятие, // в отличие от международных шашек), простая шашка бьёт в любом из // четырёх диагональных направлений (включая назад), дамка ходит и // бьёт "летающим" ходом на любое расстояние по диагонали. Официальные // правила ничьей (15/30-ходовые ограничения для эндшпилей) не // реализованы — вместо них простая защита: если долго (60 полуходов) // не было взятий, партия принудительно объявляется ничьей. // CheckersColor — цвет стороны. type CheckersColor int const ( CheckersWhite CheckersColor = iota CheckersBlack ) func (c CheckersColor) Opponent() CheckersColor { if c == CheckersWhite { return CheckersBlack } return CheckersWhite } // CheckersPiece — шашка на доске. type CheckersPiece struct { Color CheckersColor King bool } // CheckersPos — координаты клетки (0-7, 0-7). type CheckersPos struct { Row, Col int } func (p CheckersPos) inBounds() bool { return p.Row >= 0 && p.Row < 8 && p.Col >= 0 && p.Col < 8 } var ( ErrCheckersWrongTurn = errors.New("сейчас не ваш ход") ErrCheckersNoPiece = errors.New("на этой клетке нет вашей шашки") ErrCheckersMustContinue = errors.New("нужно продолжать бить той же шашкой") ErrCheckersInvalidMove = errors.New("такой ход недопустим") ErrCheckersMustCapture = errors.New("доступно взятие — простой ход запрещён") ErrCheckersGameOver = errors.New("партия уже завершена") ) // checkersMoveOption — один вариант хода для конкретной шашки: куда // встать и (если это взятие) какая клетка при этом освобождается. type checkersMoveOption struct { To CheckersPos Capture *CheckersPos } // CheckersResult — итог завершившейся партии. type CheckersResult struct { Winner CheckersColor Draw bool } // maxCheckersMovesWithoutCapture — защита от бесконечных ничейных // партий без взятий (упрощение официальных правил ничьей). const maxCheckersMovesWithoutCapture = 60 // CheckersGameState — полное состояние партии в шашки. type CheckersGameState struct { Board [8][8]*CheckersPiece Turn CheckersColor ChainPos *CheckersPos // не nil, если середина серии взятий одной шашкой MovesSinceCapture int Result *CheckersResult } // NewCheckersGame создаёт новую партию с начальной расстановкой: // белые внизу (строки 5-7), чёрные наверху (строки 0-2), только на // тёмных клетках. Первый ход — за белыми. func NewCheckersGame() *CheckersGameState { g := &CheckersGameState{Turn: CheckersWhite} for row := 0; row < 3; row++ { for col := 0; col < 8; col++ { if (row+col)%2 == 1 { g.Board[row][col] = &CheckersPiece{Color: CheckersBlack} } } } for row := 5; row < 8; row++ { for col := 0; col < 8; col++ { if (row+col)%2 == 1 { g.Board[row][col] = &CheckersPiece{Color: CheckersWhite} } } } return g } var checkersDirections = []CheckersPos{{Row: -1, Col: -1}, {Row: -1, Col: 1}, {Row: 1, Col: -1}, {Row: 1, Col: 1}} // simpleMoveOptions возвращает некапитальные ходы для шашки в pos. func (g *CheckersGameState) simpleMoveOptions(pos CheckersPos) []CheckersPos { piece := g.Board[pos.Row][pos.Col] if piece == nil { return nil } var out []CheckersPos if piece.King { for _, d := range checkersDirections { for step := 1; ; step++ { np := CheckersPos{Row: pos.Row + d.Row*step, Col: pos.Col + d.Col*step} if !np.inBounds() || g.Board[np.Row][np.Col] != nil { break } out = append(out, np) } } return out } forward := 1 if piece.Color == CheckersWhite { forward = -1 } for _, dc := range []int{-1, 1} { np := CheckersPos{Row: pos.Row + forward, Col: pos.Col + dc} if np.inBounds() && g.Board[np.Row][np.Col] == nil { out = append(out, np) } } return out } // captureMoveOptions возвращает доступные варианты взятия для шашки // в pos (без учёта того, обязательно ли вообще бить в этот ход — // это проверяется на уровне выше). func (g *CheckersGameState) captureMoveOptions(pos CheckersPos) []checkersMoveOption { piece := g.Board[pos.Row][pos.Col] if piece == nil { return nil } var out []checkersMoveOption if piece.King { for _, d := range checkersDirections { var enemyAt *CheckersPos for step := 1; ; step++ { np := CheckersPos{Row: pos.Row + d.Row*step, Col: pos.Col + d.Col*step} if !np.inBounds() { break } occupant := g.Board[np.Row][np.Col] if enemyAt == nil { if occupant == nil { continue } if occupant.Color == piece.Color { break } captured := np enemyAt = &captured continue } if occupant != nil { break } out = append(out, checkersMoveOption{To: np, Capture: enemyAt}) } } return out } for _, d := range checkersDirections { mid := CheckersPos{Row: pos.Row + d.Row, Col: pos.Col + d.Col} land := CheckersPos{Row: pos.Row + d.Row*2, Col: pos.Col + d.Col*2} if !land.inBounds() { continue } midPiece := g.Board[mid.Row][mid.Col] if midPiece == nil || midPiece.Color == piece.Color { continue } if g.Board[land.Row][land.Col] != nil { continue } out = append(out, checkersMoveOption{To: land, Capture: &mid}) } return out } // anyCaptureAvailable проверяет, есть ли у стороны color хоть одно // доступное взятие где-либо на доске. func (g *CheckersGameState) anyCaptureAvailable(color CheckersColor) bool { for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { p := g.Board[r][c] if p != nil && p.Color == color && len(g.captureMoveOptions(CheckersPos{Row: r, Col: c})) > 0 { return true } } } return false } // hasAnyLegalMove проверяет, есть ли у стороны color вообще хоть // один легальный ход (взятие или простой). func (g *CheckersGameState) hasAnyLegalMove(color CheckersColor) 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 } pos := CheckersPos{Row: r, Col: c} if len(g.captureMoveOptions(pos)) > 0 || len(g.simpleMoveOptions(pos)) > 0 { return true } } } return false } // LegalMovesFrom возвращает список допустимых клеток назначения для // шашки в pos с учётом текущей фазы хода (продолжение серии взятий, // обязательное взятие или свободный ход). func (g *CheckersGameState) LegalMovesFrom(pos CheckersPos) []checkersMoveOption { if g.Result != nil { return nil } piece := g.Board[pos.Row][pos.Col] if piece == nil || piece.Color != g.Turn { return nil } if g.ChainPos != nil { if pos != *g.ChainPos { return nil } return g.captureMoveOptions(pos) } if g.anyCaptureAvailable(g.Turn) { return g.captureMoveOptions(pos) } simples := g.simpleMoveOptions(pos) out := make([]checkersMoveOption, len(simples)) for i, s := range simples { out[i] = checkersMoveOption{To: s} } return out } func isPromotionRow(color CheckersColor, row int) bool { if color == CheckersWhite { return row == 0 } return row == 7 } // Move выполняет ход шашкой из from в to, если он допустим по // текущим правилам (см. LegalMovesFrom). func (g *CheckersGameState) Move(from, to CheckersPos) error { if g.Result != nil { return ErrCheckersGameOver } piece := g.Board[from.Row][from.Col] if piece == nil || piece.Color != g.Turn { return ErrCheckersNoPiece } if g.ChainPos != nil && from != *g.ChainPos { return ErrCheckersMustContinue } options := g.LegalMovesFrom(from) var chosen *checkersMoveOption for i := range options { if options[i].To == to { chosen = &options[i] break } } if chosen == nil { if g.ChainPos == nil && g.anyCaptureAvailable(g.Turn) { simples := g.simpleMoveOptions(from) for _, s := range simples { if s == to { return ErrCheckersMustCapture } } } return ErrCheckersInvalidMove } g.Board[from.Row][from.Col] = nil if chosen.Capture != nil { g.Board[chosen.Capture.Row][chosen.Capture.Col] = nil } g.Board[to.Row][to.Col] = piece if !piece.King && isPromotionRow(piece.Color, to.Row) { piece.King = true } if chosen.Capture != nil { g.MovesSinceCapture = 0 if len(g.captureMoveOptions(to)) > 0 { g.ChainPos = &to return nil } g.ChainPos = nil } else { g.MovesSinceCapture++ g.ChainPos = nil } g.Turn = g.Turn.Opponent() g.checkGameOver() return nil } func (g *CheckersGameState) checkGameOver() { if g.MovesSinceCapture >= maxCheckersMovesWithoutCapture { g.Result = &CheckersResult{Draw: true} return } if !g.hasAnyLegalMove(g.Turn) { g.Result = &CheckersResult{Winner: g.Turn.Opponent()} } }