Сегодня мы напишем на Python свою версию популярной логической игры - пятнашки. Цель игры заключается в том чтобы перемещая блоки в рамке упорядочить их по номерам.
Начнем с импорта нужных библиотек и задания глобальных переменных
from tkinter import Tk, Canvas from random import shuffle # Размер игрового поля (4x4) BOARD_SIZE = 4 # Размер одного блока в пикселях SQUARE_SIZE = 80 # Значение пустого блока. В нашем случае пустым будет последний блок EMPTY_SQUARE = BOARD_SIZE ** 2 # Главное окно root = Tk() root.title("Pythonicway Fifteen") # Область для рисования c = Canvas(root, width=BOARD_SIZE * SQUARE_SIZE, height=BOARD_SIZE * SQUARE_SIZE, bg='#808080') c.pack() root.mainloop()
В качестве пятнашек у нас будет выступать список c целыми числами.
board = list(range(1, EMPTY_SQUARE + 1))
Теперь напишем функцию, которая будет отрисовывать пятнашки из списка:
def draw_board(): # убираем все, что нарисовано на холсте c.delete('all') # Наша задача сгруппировать пятнашки из списка в квадрат # со сторонами BOARD_SIZE x BOARD_SIZE # i и j будут координатами для каждой отдельной пятнашки for i in range(BOARD_SIZE): for j in range(BOARD_SIZE): # получаем значение, которое мы должны будем нарисовать # на квадрате index = str(board[BOARD_SIZE * i + j]) # если это не клетка которую мы хотим оставить пустой if index != str(EMPTY_SQUARE): # рисуем квадрат по заданным координатам c.create_rectangle(j * SQUARE_SIZE, i * SQUARE_SIZE, j * SQUARE_SIZE + SQUARE_SIZE, i * SQUARE_SIZE + SQUARE_SIZE, fill='#43ABC9', outline='#FFFFFF') # пишем число в центре квадрата c.create_text(j * SQUARE_SIZE + SQUARE_SIZE / 2, i * SQUARE_SIZE + SQUARE_SIZE / 2, text=index, font="Arial {} italic".format(int(SQUARE_SIZE / 4)), fill='#FFFFFF')
Не забудьте вызвать функцию отрисовки игрового поля перед вызовом root.mainloop()
Добавляем обработчик событий. При клике на клеточку мы поменяем ее местами с пустой клеткой.
def click(event): # Получаем координаты клика x, y = event.x, event.y # Конвертируем координаты из пикселей в клеточки x = x // SQUARE_SIZE y = y // SQUARE_SIZE # Получаем индекс в списке объекта по которому мы нажали board_index = x + (y * BOARD_SIZE) # Получаем индекс пустой клетки в списке. Эту функцию мы напишем позже empty_index = get_empty_neighbor(board_index) # Меняем местами пустую клетку и клетку, по которой кликнули board[board_index], board[empty_index] = board[empty_index], board[board_index] # Перерисовываем игровое поле draw_board() # Если текущее состояние доски соответствует правильному - рисуем сообщение о победе if board == correct_board: # Эту функцию мы добавим позже show_victory_plate()
Привязываем обработчик событий к холсту
c.bind('<Button-1>', click)
Теперь нужно добавить функцию get_empty_neighbor, которая будет возвращать индекс блока с которым мы хотим поменять местами наш блок. Эта функция может показаться немного сложной, поэтому постараюсь сначала описать основную идею. Мы будем менять клетки местами, только если рядом с клеткой на которую мы нажали будет пустая клетка. Рядом означает сверху, снизу, слева или справа. Структурой данных для представления всех блоков мы выбрали одномерный список. В таком случае, сверху и снизу означает на расстоянии размера доски. Например, 7 клетка на расстоянии 4 от 11, соответственно 11 находится под 7. Слева и справа означает, что клетка на расстоянии 1 от заданной. Например, 8 справа от 7, а 6 - слева. Единственное, о чем осталось позаботиться - это ситуация, когда клетка на расстоянии 1, но на следующем ряду, например, 8 и 9, в такой ситуации, мы не хотим менять их местами. Если блоки поменять невозможно - просто вернем блок, по которому нажали.
def get_empty_neighbor(index): # получаем индекс пустой клетки в списке empty_index = board.index(EMPTY_SQUARE) # узнаем расстояние от пустой клетки до клетки по которой кликнули abs_value = abs(empty_index - index) # Если пустая клетка над или под клектой на которую кликнули # возвращаем индекс пустой клетки if abs_value == BOARD_SIZE: return empty_index # Если пустая клетка слева или справа elif abs_value == 1: # Проверяем, чтобы блоки были в одном ряду max_index = max(index, empty_index) if max_index % BOARD_SIZE != 0: return empty_index # Рядом с блоком не было пустого поля return index
Теперь добавим функцию show_victory_plate, которая будет выводить сообщение о победе на экран.
def show_victory_plate(): # Рисуем черный квадрат по центру поля c.create_rectangle(SQUARE_SIZE / 5, SQUARE_SIZE * BOARD_SIZE / 2 - 10 * BOARD_SIZE, BOARD_SIZE * SQUARE_SIZE - SQUARE_SIZE / 5, SQUARE_SIZE * BOARD_SIZE / 2 + 10 * BOARD_SIZE, fill='#000000', outline='#FFFFFF') # Пишем красным текст Победа c.create_text(SQUARE_SIZE * BOARD_SIZE / 2, SQUARE_SIZE * BOARD_SIZE / 1.9, text="ПОБЕДА!", font="Helvetica {} bold".format(int(10 * BOARD_SIZE)), fill='#DC143C')
Чтобы запустить игру нам нужно добавить несколько последних штрихов.
# Создаем список блоков board = list(range(1, EMPTY_SQUARE + 1)) # Список с которым мы будем сравнивать результат. В данном случае это # просто отсортированный список, но при желании можно придумать что-то другое correct_board = board[:] # перемешиваем блоки shuffle(board) # рисуем доску draw_board()
В принципе, можно играть уже в эту версию игры, но у нас все еще есть одна проблема. Если мы просто перемешаем блоки, то может возникнуть ситуация, в которой у головоломки не будет решения. Причина этого сама по себе интересная математическая задача, мы же просто добавим код, который будет проверять решаема ли конкретная конфогурация и если нет - перемешивать блоки заново. Алгоритм определения решаема ли задача следующий:
- Для нечетного размера поля, головоломка решаема, если количество необходимых перемещений парно.
- Для четного размера поля, головоломка решаема если
- Пустой блок находится на парном (втором, четвертом, шестом...) ряду снизу и количество перемещений непарно.
- Пустой блок на непарном (первый, третий, пятый...) ряду снизу и количество перемещений парно.
- Во всех остальных случаях головоломка не имеет решения.
def get_inv_count(): """ Функция считающая количество перемещений """ inversions = 0 inversion_board = board[:] inversion_board.remove(EMPTY_SQUARE) for i in range(len(inversion_board)): first_item = inversion_board[i] for j in range(i+1, len(inversion_board)): second_item = inversion_board[j] if first_item > second_item: inversions += 1 return inversions def is_solvable(): """ Функция определяющая имеет ли головоломка рещение """ num_inversions = get_inv_count() if BOARD_SIZE % 2 != 0: return num_inversions % 2 == 0 else: empty_square_row = BOARD_SIZE - (board.index(EMPTY_SQUARE) // BOARD_SIZE) if empty_square_row % 2 == 0: return num_inversions % 2 != 0 else: return num_inversions % 2 == 0
Теперь добавим только код перемешивания блоков и игра готова
while not is_solvable(): shuffle(board)
Исходный код игры на гитхаб