Pull to refresh

Go напишем шахматный сервер? Часть вторая — структуры, интерфейсы и методы

Level of difficultyEasy
Reading time5 min
Views2.5K

В предыдущей части были сформулированы общие для всех фигур свойства и основные алгоритмы, которые позволят нам анализировать ситуацию на доске. Вот только как всё это реализовать в коде?

Фигуры

В 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

Tags:
Hubs:
Total votes 7: ↑7 and ↓0+9
Comments14

Articles