Пишем игру шашки на Python

Пишем игру шашки на Python

Сегодня мы создадим логику для игры шашки, с помощью скриптового языка программирования Python. Я пока не буду описывать алгоритм AI для этой игры (он же подойдёт для шахмат). Рассмотрим только организацию игровой доски а проверку ходов на валидность.

Немного истории и правил игры

Шашки это игра для двух игроков на доске, подобной шахматной, специальными фишками. Чаще всего, используют поле 8х8, 12 фишек (русские шашки). Одна фишка может годить по диагонали на 1 клетку, бить на вперёд и назад 2 и больше. Ход назад запрещён. После достижения последнего, противоположного поля, фишка превращаются в дамку, которая может ходить по диагонали в любом направлении на любое количество ходов.

Организация доски

Много кто мне предлагал взять простой(одномерный) массив со строками/числами и всячески извращаться для доступа к нужной части его. Мы сейчас живём в 21 веке, зачем экономить на каждом байте, для этого разве мы покупаем новые компьютеры. Поэтому Вся наша доска состоит из клеток – объектов класса gameSquare. Каждый из этих объектов может содержать в себе объект класса piece – шашку.

Не забудем отметить, что есть такие поля, по которым ни одна шашка за всю игру не сможет ходить, поэтому нам нужно будет также добавить поле, которые показывает, возможен ли ход в эту часть доски.

Класс шашки class piece(object): def __init__(self): self. player = None # Какому игроку принадлежит шашка self. alive = False # Жива ли ещё она (не побили) self. king = False # В дамках?

Self. symbol = '' # Для дебаг-вывода, обозначение шашки символом

Класс клетки игровой доски class gameSquare(object): def __init__(self): self. validSquare = False # Можно ли ходить на эту клетку self. occupied = False # Занята ли клетка шашкой self. occupier = piece() # Если клетка занята, указатель на шашку def printSymbol(self): print self. occupier. symbol, # Выводим состояние текущей клетки

Класс игровой доски class gameBoard(object): def __init__(self): self._prepareBoard() # Подготовка доски (создание объектов клетки) self._validateSquares() # Метка клеток, по которым можно ходить self._populateSquares() # Расстановка шашек

Как всегда, разделяем всю работу по функциям и вызываем их в конструкторе. Перейдём к определению первой функции:

# Создаём двумерный массив [8][8] def _prepareBoard(self): self._matrix = [] for i in xrange(8): self._matrix. append( [gameSquare() for _ in xrange(8)] )

Теперь определение клеток, по которым можно ходить фишкам: def _validateSquares(self): for row in xrange(8): for col in xrange(8): if self._darkQuad(row, col) == True: self._matrix[row][col].validSquare = False self._matrix[row][col].occupier. symbol = '.' else: self._matrix[row][col].validSquare = True self._matrix[row][col].occupier. symbol = '.'

Здесь используется функция _darkQuad(row, col). Она определяет, можно ли ходить на клетку по заданным координатам (ходить можно только по светлым клеткам поля).

Def _darkQuad(self, row, col): return ((row%2) == (col%2))

Чтобы понять суть операции, достаточно взглянуть на доску. И теперь одна из самых важных функций – расстановка шашек по доске:

# Tick areas on the board (shapes, empty, unmovable) def _populateSquares(self): for row in xrange(8): for col in xrange(8):

# Make up black squares if self._matrix[row][col].validSquare and row <= 2: self._matrix[row][col].occupied = True self._matrix[row][col].occupier. symbol = 'x' self._matrix[row][col].occupier. alive = True self._matrix[row][col].occupier. king = False self._matrix[row][col].occupier. player = 2

# Make up white squares if self._matrix[row][col].validSquare and row >= 5: self._matrix[row][col].occupied = True self._matrix[row][col].occupier. symbol = 'o' self._matrix[row][col].occupier. alive = True self._matrix[row][col].occupier. king = False self._matrix[row][col].occupier. player = 1

# Fill empty quads with empty space if self._matrix[row][col].validSquare and row > 2 and row < 5: self._matrix[row][col].ocuppied = False

Начиная с 6-й строки кода, мы проверяем, если клетка находится в первых двух рядах и туда можно поставить фишку, значит она принадлежит игру с чёрным цветом.

В конце функции не забываем заполнить ряды между чёрными и белыми фишками. Доска создана! Теперь запишем функцию, которая будет выполнять debug-вывод доски: def _printDebugBoard(self): for row in xrange(8): for col in xrange(8): self._matrix[row][col].printSymbol() print ''

Если мы сейчас создадим объект доски и запустим дебаг-вывод board = gameBoard() board._printDebugBoard() то получим:

. x. x. x. x x. x. x. x.

. x. x. x. x

. . . . . . . .

. . . . . . . .

O. o. o. o.

. o. o. o. o o. o. o. o.

‘o’ – белые шашки, ‘x’ – чёрные.

Организация ходов по доске

Доска готова, поэтому теперь нам нужно описать функции для совершения ходов шашками. Правила игры мы повтори вначале, поэтому приступим.

Для начала введём пару дополнительных переменных в функции __main__ основной программы: if __name__ == '__main__':

# Активный игрок player = 1

# Координаты фишки, которую нужно передвинуть old_row = 5 old_col = 4

# Координаты, куда нужно совершить ход new_row = 4 new_col = 5 board = gameBoard() board._printDebugBoard()

Как видите, у нас есть координаты шашки, которую нужно переместить. Первое, что нужно сделать – проверить, есть ли там шашка вообще.

Проверка существования шашки в заданной клетке def occupySquare(self, player, row, col): return ((self._matrix[row][col].occupier. player == player) and

(self._matrix[row][col].occupier. alive))

Благодаря нашей структуре доски, все эти действия выполнять очень легко. Мы просто проверяем существование шашки в нужных координатах и её отношение к игроку.

Проверка клетки, куда мы собираемся сделать ход def squareOccupied(self, new_row, new_col): return self._matrix[new_row][new_col].occupied

Если пустая – можно далее продвигаться в совершении хода.

Проверка правильности хода со старой позиции в новую def validDirection(self, old_row, old_col, new_row, new_col, player):

# Vertical movement if not self._matrix[old_row][old_col].occupier. king: if player == 1: if not (old_row > new_row and (old_row - new_row) <= 2): return False elif player == 2: if not (old_row < new_row and (new_row - old_row) <= 2): return False elif self._matrix[old_row][old_col].occupier. king: if math. fabs(old_row - new_row) > 2: return False

# Horizontal movement if math. fabs(old_row - new_row) != math. fabs(old_col - new_col): return False return True

Опять же, чтобы яснее понимать суть происходящего, желательно посмотреть на игровую доску. Сразу мы проверяем правильность передвижения по вертикали (y-координаты). Обрабатываем возможность быть дамкой. Суть проверок в том, что передвижение не может быть больше, чем на 2 клетки (простой ход и бой). Возможность “перепрыгивания” мы пока не рассматриваем.

Обработка простого хода

Проверяем изменения в y-координате, при ходе. Если расстояние = 1, значит это простой ход (не бой).

If math. fabs(old_row - new_row) == 1: board. movePiece(old_row, old_col, new_row, new_col)

И сама функция перемещения movePiece: def movePiece(self, old_row, old_col, new_row, new_col):

# Занимаем новую клетку, копируем указатель на шашку в клетку self._matrix[new_row][new_col].occupied = True self._matrix[new_row][new_col].occupier = self._matrix[old_row][old_col].occupier

# Старое место теперь свободно self._matrix[old_row][old_col].occupied = False self._matrix[old_row][old_col].occupier. alive = False

Самое простое закончилось. Теперь мы можем без проблем делать одиночные ходы. Давайте запишем всё это и проверим: if __name__ == '__main__': player = 1 old_row = 5 old_col = 4 new_row = 4 new_col = 5 board = gameBoard() board._matrix[old_row][old_col].occupier. symbol = '!' board._matrix[new_row][new_col].occupier. symbol = '&' board._printDebugBoard() for i in range(1):

# These should be in game cycle if not board. occupySquare(player, old_row, old_col): print 'Player ' + str(player) + ' doesn't have square at given coordinates!' continue if board. squareOccupied(new_row, new_col): print 'Player ' + str(player) + ' can't move the square to given coordinates!' continue if not board. validDirection(old_row, old_col, new_row, new_col, player): print 'Player ' + str(player) + ' chosen movement is invalid!' continue

# If chosen movement is just a single step if math. fabs(old_row - new_row) == 1: board. movePiece(old_row, old_col, new_row, new_col)

Мы просто поместили все проверки в нужном порядке. Не забывайте, что всё это должно происходить в игровом цикле. Для наглядности, я позначил символом ‘!’ шашку, которая будет совершать ход, ‘&’ – куда она будет ходить.

. x. x. x. x x. x. x. x.

. x. x. x. x

. . . . . . . .

. . . . . & . .

O. o. ! . o.

. o. o. o. o o. o. o. o.

Ошибок не возникает, потому что ход верный. Давайте немного изменим координаты точки, в которую нужно поставить шашку: new_row = 3 new_col = 4

. x. x. x. x x. x. x. x.

. x. x. x. x

. . . . & . . .

. . . . . . . .

O. o. ! . o.

. o. o. o. o o. o. o. o.

Player 1 chosen movement is invalid!

Обработка боя if math. fabs(old_row - new_row) == 2: if not board. checkJump(old_row, old_col, new_row, new_col, player): print 'Player + ' + str(player) + ' jump is invalid moving' continue else: board. jumpPiece(old_row, old_col, new_row, new_col)

Проверяем изменения в y-координате. Как мы помним, 2, при бое. Если так, то проверяем возможность удара. Функция checkJump достаточно большая и громоздкая, но другого выхода нет: def checkJump(old_row, old_col, new_row, new_col, player):

# Бой вперёд if old_row > new_row: if old_col > new_col:

# Если бъем вправо if self._matrix[old_row-1][old_col-1].occupied or self._matrix[old_row-1][old_col-1].occupier. player == player: return False elif old_col < new_col:

# Бьем влево if self._matrix[old_row-1][old_col+1].occupied or self._matrix[old_row-1][old_col+1].occupier. player == player: return False

# Бой назад elif old_row < new_row: if old_col > new_col: if self._matrix[old_row+1][old_col-1].occupied or self._matrix[old_row+1][old_col-1].occupier. player == player: return False elif old_col < new_col: if self._matrix[old_row+1][old_col+1].occupied or self._matrix[old_row+1][old_col+1].occupier. player == player: return False return True

Попробуйте сами разобрать её структуру и назначение.

Def jumpPiece(old_row, old_col, new_row, new_col):

# Занимаем клетку, куда собираемся идти self._matrix[new_row][new_col].occupied = True self._matrix[new_row][new_col].occupier = self._matrix[old_row][old_col].occupier

# Освобождаем старую self._matrix[old_row][old_col].occupied = False self._matrix[old_row][old_col].occupier. alive = False

# Вычисляем координаты фишки, которую мы побили if old_row > new_row: jumpRow = old_row - 1 if old_col > new_col: jumpCol = old_col - 1 elif old_col < new_col: jumpCol = old_col + 1 elif old_row < new_row: jumpRow = old_row + 1 if old_col > new_col: jumpCol = old_col - 1 elif old_col < new_col: jumpCol = old_col + 1

# Битую фишку позначаем, как неактивную self._matrix[jumpRow][jumpCol].occupied = False self._matrix[jumpRow][jumpCol].occupier. alive = False

Теперь наши шашки могут бить друг друга. Единственное, нам осталось проверять шашку на дамку после каждого хода: if player == 1 and new_row == 0 or player == 2 and new_row == 7: board. kingMe(new_row, new_col, player)

Если каждый из игроков достиг противоположной стороны, делаем его шашку дамкой: def kingMe(row, col, player): self._matrix[row][col].occupier. king = True if player == 1: self._matrix[row][col].occupier. symbol = 'O' elif player == 2: self._matrix[row][col].occupier. symbol = 'X'

Последнее, что нам нужно сделать – сменить номер активного игрока и проверить состояние игры, может кто-либо уже победил. В тот же цикл добавляем: player = (player%2) + 1 board. gameEnded(player): print 'Player: ' + str(player) + ' has won the game'

А, вот, функция gameEnded, наверное, одна из самых тяжёлых. Мы должны проверить, есть ли ещё шашки противника, возможно ли ещё хода в игре и т. д. Она состоит из нескольких дополнительных функций. Сразу разберём её: def gameEnded(player): moves = 0 for row in xrange(8): for col in xrange(8): if self._matrix[row][col].validSquare and self._matrix[row][col].occupied and self._matrox[row][col].occupier. player == player and

( self. moveSingleSpace(player, row, col) or self. jumpAvailable(player, row, col, 0, 0, 0)

): moves += 1 if not piecesLeft(player): return True elif not moves: return True else: return False

Заводим счётчик moves, он показывает ещё количество возможных ходов. Из новых функций: piecesLeft – остались ли ещё шашки moveSingleSpace – есть ли ещё хода для какой-либо шашки jumpAvailable – может ли какая-либо шашка сделать бой

Самая простая – piecesLeft, с неё и начнём: def piecesLeft(player): pieces = 0 for row in xrange(8): for col in xrange(8): if self._matrix[row][col].validSquare and self._matrix[row][col].occupied and self._matrix[row][col].occupier. player == player: pieces += 1 return pieces

Проходим циклом по всем шашкам и проверяем их активность. Эта функция, как уже говорили, проверяет возможность хода для заданной шашки: def moveSingleSpace(player, row, col): if

( not self._matrix[row-1][col-1].occupied and not (row-1) < 0 and not (col-1) < 0 and self. validDirection(row, col, row-1, col-1, player)

) or

( not self._matrix[row-1][col+1].occupied and not (row-1) < 0 and not (col+1) > 7 and self. validDirection(row, col, row-1, col+1, player)

) or

( not self._matrix[row+1][col-1].occupied and not (row+1) > 7 and not (col-1) < 0 and self. validDirection(row, col, row+1, col-1, player)

) or

( not self._matrix[row+1][col+1].occupied and not (row+1) > 7 and not (col+1) > 7 and self. validDirection(row, col, row+1, col+1, player)

): return True else: return False

Возможность боя для заданной шашки: def jumpAvailable(player, row, col, flag, old_row, old_col): if

( not self._matrix[row-2][col-2].occupied and not (row-2) < 0 and not (col-2) < 0 and self. validDirection(row, col, row-2, col-2, player) and self. checkJump(row, col, row-2, col-2, player)

) or

( not self._matrix[row-2][col+2].occupied and not (row-2) < 0 and not (col+2) > 7 and self. validDirection(row, col, row-2, col+2, player) and self. checkJump(row, col, row-2, col+2, player)

) or

( not self._matrix[row+2][col-2].occupied and not (row+2) > 7 and not (col-2) < 0 and self. validDirection(row, col, row+2, col-2, player) and self. checkJump(row, col, row+2, col-2, player)

) or

( not self._matrix[row+2][col+2].occupied and not (row+2) > 7 and not (col+2) > 7 and self. validDirection(row, col, row+2, col+2, player) and self. checkJump(row, col, row+2, col+2, player)

): return True else: return False

Заключение

На этом всё! Теперь можно играть в шашки, передавая в цикл координаты ходов. Дополнить скрипт циклом уже не так сложно, поэтому вы вполне справитесь сами с этой задачей. Чуть позже я опишу алгоритмы MiniMax, который используют в шашках, шахматах для создания AI.


Карта сайта


Информационный сайт Webavtocat.ru