Don't learn to code. Code to learn!

Сегодня мы напишем на 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()

В принципе, можно играть уже в эту версию игры, но у нас все еще есть одна проблема. Если мы просто перемешаем блоки, то может возникнуть ситуация, в которой у головоломки не будет решения. Причина этого сама по себе интересная математическая задача, мы же просто добавим код, который будет проверять решаема ли конкретная конфогурация и если нет - перемешивать блоки заново. Алгоритм определения решаема ли задача следующий:

  1. Для нечетного размера поля, головоломка решаема, если количество необходимых перемещений парно.
  2. Для четного размера поля, головоломка решаема если
    • Пустой блок находится на парном (втором, четвертом, шестом...) ряду снизу и количество перемещений непарно.
    • Пустой блок на непарном (первый, третий, пятый...) ряду снизу и количество перемещений парно.
  3. Во всех остальных случаях головоломка не имеет решения.
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)

 Игра пятнашки в процессе

 Исходный код игры на гитхаб