327 lines
9.9 KiB
Go
327 lines
9.9 KiB
Go
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()}
|
||
}
|
||
}
|