В предыдущей части были сформулированы общие для всех фигур свойства и основные алгоритмы, которые позволят нам анализировать ситуацию на доске. Вот только как всё это реализовать в коде?
Фигуры
В Go можно представить некий объект как структуру:
type BaseFigure struct {
IsWhite bool
Type byte
CellCoordinates [2]int
}
Структура BaseFigure
содержит информацию о цвете, типе и координатах фигуры. Для этой обезличенной фигуры мы можем создавать различные методы (функции, работающие с конкретной структурой). Например, метод меняющий координаты фигуры:
func (figure *BaseFigure) ChangeCoordinates(newCoordinates [2]int) {
figure.CellCoordinates = newCoordinates
}
Но зачем нужна обезличенная фигура? Почему нельзя сразу создать какую-нибудь пешку?
Тут дело в том, что точно так же как мы можем написать метод, меняющий координаты фигуры, можно написать метод, ищущий её возможные ходы. Вот только такой метод будет отличаться для разных типов фигур (ведь ходят они все по‑разному). А вот метод смены координат одинаков для любой фигуры.
Поэтому структура пешки будет выглядеть так:
type Pawn struct {
BaseFigure
}
Метод поиска ходов пешки будет реализован для структуры Pawn
, а метод для смены координат для структуры BaseFigure
и его не нужно прописывать каждый раз для каждого типа фигур. Методы BaseFigure
будут просто наследоваться структурами конкретных фигур.
Так что есть фигура у нас в коде? BaseFigure
или Pawn
? Ни то и ни другое. Это интерфейс:
type Figure interface {
IsItWhite() bool
GetType() byte
GetPossibleMoves(*Game) *TheoryMoves
ChangeCoordinates([2]int)
GetCoordinates() [2]int
Delete()
}
То есть набор методов для вышеописанных структур. Именно такие интерфейсы будут прикреплены к конкретным координатам доски и с их помощью можно выудить всю необходимую информацию о фигуре, а также перемещать или вовсе удалить её.
Как мы видим, тут есть методыBaseFigure
(такие как GetType
) и методы конкретной фигуры (GetPossibleMoves
).
Создаётся фигура следующим образом:
func CreateFigure(_type byte, isWhite bool, coordinates [2]int) Figure {
var bf = BaseFigure{isWhite, _type, coordinates}
switch _type {
case 'p':
return &Pawn{bf}
...
}
Игра
Внимательный читатель мог заметить в интерфейсе Figure
нечто Game
. Это состояние игры на n-ом ходу:
type Game struct {
Figures map[int]*Figure
IsCheckWhite IsCheck
IsCheckBlack IsCheck
WhiteCastling Castling
BlackCastling Castling
LastPawnMove *int
Side bool
}
Это тот необходимый минимум информации для анализа хода.
Figures
- это фигуры на доскеСтруктуры
IsCheck
содержат информацию о том, есть ли шах сейчас на доскеCastling
о рокировкеLastPawnMove
о том, где находится пешка, сделавшая двойное перемещение ходом ранее (nil
если предыдущий ход был иным)Side
— чей сейчас ход (чёрных или белых)
Координаты фигуры сохраняются как число от 0 до 63 (по числу полей на доске). Это число (id поля) при анализе трансформируется в координаты (x,y). Поэтому тут мы видим, что LastPawnMove
— это *int
, а не *[]int
.
Планировалось ими (id полей) и оперировать при анализе. Но оказалось, что это очень неудобно, так как двумерное пространство мы пытаемся представить как одномерное. Как в такой ситуации понять, что id равное 8 это не первое поле во втором ряду, а на самом деле девятое в первом (то есть несуществующее на доске)? Решить эту проблему (не прибегая к координатам) можно, но не нужно.
Аналогично и с Figures
, где id поля на доске это ключ, по которому достаётся фигура с этого поля (или nil
если фигуры там нет).
Ход и шах
Пройдёмся теперь по реализации двух алгоритмов из прошлой части IsMoveCorrect
и IsItCheck
func IsMoveCorrect(gameModel models.Game, board models.Board, from int, to int) ([]int, Game) {
game := CreateGameStruct(gameModel, board)
figure := game.GetFigureByIndex(from)
if !game.IsItYourFigure(figure) {
return []int{}, Game{}
}
possibleMoves := (*figure).GetPossibleMoves(&game)
isCorrect, indexesToChange := CheckMove(possibleMoves, []int{from, to})
if !isCorrect {
return []int{}, Game{}
}
return indexesToChange, game
}
На момент вызова этой функции уже известно следующее:
пользователь, запрашивающий ход, это один из игроков и сейчас его ход
ход не противоречит устройству доски (поля
from
иto
реально существуют)текущее состояние игры —
gameModel
(информация о рокировках, предыдущем ходе и цвете ходящего игрока)доска —
board
(массив из пар id поля и id фигуры)
Теперь нужно создать из первых двух аргументов структуру game := CreateGameStruct(gameModel, board)
Вытягиваем из game
фигуру, которой будет совершаться ход, и проверяем, что эта фигура не nil
(на этом поле есть фигура) и принадлежит игроку (проверка цвета).
Теперь есть всё необходимое для получения possibleMoves
— массива возможных ходов для этой фигуры, но пока без проверки на шах ходящему игроку.
В дальнейшем можно будет использовать этот массив для создания дерева возможных ходов. Это позволит сделать систему подсказок, а оценка каждого хода и вовсе добавит возможность сыграть против машины.
CheckMove
сверяет запрашиваемый ход с массивом возможных. Массив indexesToChange
, который она возвращает, включает всё те же from
и to
— индексы полей, для которых надо будет совершить перестановку фигуры. Но в случае с рокировкой или взятием на проходе у нас происходят изменения для большего количества полей. Поэтому indexesToChange
может нести в себе больше информации, чем при простом ходе.
func IsItCheck(indexesToChange []int, game *Game) bool {
from := indexesToChange[0]
to := indexesToChange[1]
game.ChangeToAndFrom(to, from)
if len(indexesToChange) > 2 {
game.DeletePawn(indexesToChange)
game.ChangeRookField(indexesToChange)
}
game.ChangeKingGameId(to)
if game.Check() {
return false
}
game.ChangeCastlingFlag(to)
game.ChangeLastPawnMove(from, to)
return true
}
IsItCheck
идёт следом за IsMoveCorrect
и проверяет game
на состояние шаха. Структура Game
уже создана, но она всё ещё соответствует предыдущему ходу.
game.ChangeToAndFrom(to, from)
совершает "перестановку" ходящей фигуры. Сама фигура на самом деле не двигается. Просто одна удаляется (при взятии), а у второй (которая ходит) меняются координаты.
Если ход это рокировка или взятие на проходе, то длина indexesToChange
больше двух и нужно либо удалить пешку противника, либо "переставить" ладью.
Если ходящая фигура это король, то сохраняем его текущее местоположение в game
. Тогда нам не придётся искать его каждый раз при проверке на шах.
game.Check()
— проверяем, есть ли шах на доске или нет по алгоритму, описанному в предыдущей статье.
Если ход корректен, то меняем и сохраняем информацию о рокировке и двойном перемещении пешки (если они имели место быть в текущем ходе).
Всё! Теперь можно сохранять в базу результаты работы (или возвращать ошибку, если ход был некорректным).
Что дальше?
Мы разобрались с тем, как реализована на Go основная механика игры. Однако совсем не поговорили о том, как в принципе работает сервер. Как и где сохраняется информация, как выглядит архитектура приложения и т.д.
Об этом в следующий раз)
Ссылки
Проект находится в разработке, буду рад новым идеям и тем, кому было бы интересно заняться фронтовой частью.
В данный момент я ищу работу Golang разработчиком, очень жду ваших сообщений мне в тг: t.me/Gekko_Moria
Комментарии (13)
yeswell
29.05.2024 10:19Какие ещё проблемы возникают при анализе хода, если оперировать координатами фигур в виде чисел от 0 до 63?
На Хабре есть статья, где анализ проводят именно в таком видеIvanChernetskiy Автор
29.05.2024 10:19+2При анализе возникает неопределённость. Допустим, мы хотим посмотреть, куда может двигаться ладья. Напишем алгоритм, который ищет поля, чьи id соответствуют движению по вертикали и горизонтали. То есть написать какой-нибудь for, который будет имитировать движение по доске (а в реальности по массиву id) Но рано или поздно мы подходим к краю доски. Следовательно, по этому направлению мы должны остановить "движение". В случае с координатами всё просто - x и y меняются от 0 до 7. Но в случае с одномерным массивом мы не можем просто так понять вышли ли мы за пределы доски. Так как фигура просто телепортируется за на противоположный конец поля (например ладья с поля 7 на поле 8).
Решить эту проблему можно. Например, анализировать не поле 8 на 8, а 12 на 12. То есть создать ''буферную зону" по краям, чьи id будут однозначно говорить, что это поле не игровое. 12 на 12 потому что конь прыгает через два ряда.
Но зачем столько мороки, когда можно анализировать просто координаты?)yeswell
29.05.2024 10:19Согласен, использование одномерных координат выглядит для анализа и правда выглядит переусложнённым
Zibx
29.05.2024 10:19+1Сейчас координаты каждой фигуры занимают 16 байт в памяти, а могли бы занимать 1. Можно использовать не номера ячеек на доске, а паковать XXXXYYYY в один байт. Любой алгоритм анализа будет очень рад если значения будут умещаться в регистры процессора.
IvanChernetskiy Автор
29.05.2024 10:19+1Изначально я писал шахматную логику "вслепую", было интересно самому реализовать всё, не подсматривая, и по-спотыкаться о подводные камни. Потом уже стал сравнивать)
Мне очень понравилась идея записи состояния игры с личесс. Раньше я там просто играл и не пользовался дополнительным функционалом. Можно зайти в 'инструменты' -> 'редактор доски' и увидеть записи вида:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
Это состояние соответствует начальной расстановке фигур на доске)GlukKazan
29.05.2024 10:19Там есть вещи до которых очень сложно самому додуматься. Очень много людей думало и продолжительное время. В любом случае, стоит ознакомиться хотя бы по верхам.
GlukKazan
29.05.2024 10:19rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
Очень удобная нотация. И пригодная не для одних только Шахмат.
ALexKud
29.05.2024 10:19Это конечно интересно, но это механика. Гораздо сложнее придумать эвристики оценочной функции для ограничения перебора ходов. По моему убеждению шахматы это на 90% геометрия и на 10% комбинаторика. У меня даже возникла идея разработки движка на SQL/noSQL под это предположение. Доска как таблица, позиция как ячейки-паттерны с узлами различных свойств и весов. А идея такова что нужно определить, возможна ли такая эвристика, которая оценивает позицию на один ход без перебора ходов в процессе игры. Комбинаторика, или перебор ходов включается только тогда когда эвристики скажут что здесь нужно считать. Допустим когда есть предпосылки комбинации или перехода в выигранный эндшпиль и т д и тп. Вот это было бы интересно для меня. В шахматы играю давно, есть победа в сеансе над мастером. Имел когда-то первый разряд. Играю на личесс через смартфон, но больше в процессе перемещения на работу и обратно. Время течёт незаметно тогда. На SQL с логикой в хранимых процедурах разработал несколько систем, одна из них описана в моей статье на этом сайте.
IvanChernetskiy Автор
29.05.2024 10:19О, а можно ссылку на вашу статью? Было бы интересно почитать.
Про шахматы по дороге это жиза. Только я с телефона на chess.com играю. А на личесс с ноута в комфортных условиях, поэтому там рейтинг выше)
ALexKud
29.05.2024 10:19Тапните по моему Нику в сообщении, на моей странице публикация на хабре. Ссылка вставляется но сообщение не отправляется
Lev3250
На го надо писать сервер для..... го
IvanChernetskiy Автор
Так и не полюбилась эта игра(
Шахматы мне куда ближе. Но на реализацию го на go я бы глянул хотя бы из любопытства)