go_games_collection/checkers_game.go
2026-07-04 13:51:04 +03:00

327 lines
9.9 KiB
Go
Raw Permalink 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"
// ПРИМЕЧАНИЕ О ПРАВИЛАХ: реализованы русские шашки (не международные
// и не английские): доска 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()}
}
}