Узнайте, как алгоритм обратной раздачи гарантирует проходимость в Mahjong Solitaire. Реализация на Python с примерами кода. Попробуйте сами!
Вы когда-нибудь играли в Mahjong Solitaire и через 20 минут понимали, что не можете сделать ни одного хода? Это не ваша ошибка — случайная раздача часто приводит к нерешаемым комбинациям. В этой статье мы разберем алгоритм, который гарантирует, что каждая партия будет проходимой.
Прямая раздача (просто перемешать 144 плитки и разместить их на поле) кажется очевидной, но она часто порождает нерешаемые расклады. Четыре последние плитки могут оказаться погребёнными друг под другом, а две одинаковые — запертыми в позициях, которые никогда не станут свободными одновременно. Вы узнаете об этом не в начале, а спустя 20 минут игры, когда ничего не совпадает и нет пути назад.
Вместо того чтобы генерировать доску и проверять её на проходимость, мы сначала создаём решение, а затем выводим из него доску. Алгоритм состоит из двух шагов:
Повторение этого порядка очищает доску. Таким образом, полное решение существует ещё до того, как игрок начнёт играть.
Плитка считается свободной, если сверху ничего нет и хотя бы одна длинная сторона (левая или правая) открыта.
def is_free(i, gone_set):
# Получаем координаты плитки
z, hx, hy = tiles[i]
# Проверяем, есть ли плитка на соседних позициях
def there(z, hx, hy):
j = index.get(f"{z}|{hx}|{hy}")
if j is None:
return False
return j not in gone_set
# Проверка, что сверху ничего нет
for dx in range(-1, 2):
for dy in range(-1, 2):
if there(z + 1, hx + dx, hy + dy):
return False
# Проверка открытости хотя бы одной длинной стороны
left_open = not (there(z, hx - 2, hy - 1) or there(z, hx - 2, hy) or there(z, hx - 2, hy + 1))
right_open = not (there(z, hx + 2, hy - 1) or there(z, hx + 2, hy) or there(z, hx + 2, hy + 1))
return left_open or right_open
Координаты заданы в полуплиточных единицах, поэтому смещения равны ±1 и ±2: плитка покрывает две полуединицы по каждой оси, а полуединицы позволяют верхней плитке черепахи располагаться по центру блока 2×2 снизу.
def removal_order(idxs):
for attempt in range(60):
gone = set([i for i in range(len(tiles)) if i not in idxs])
order = []
stuck = False
while len(gone) < len(tiles):
free = [i for i in idxs if i not in gone and is_free(i, gone)]
if len(free) < 2:
stuck = True
break
a = random.choice(free)
b = a
while b == a:
b = random.choice(free)
order.append((a, b))
gone.add(a)
gone.add(b)
if not stuck:
return order
return None # fallback
Цикл повторных попыток нужен, потому что жадный случайный выбор может завести в тупик — останутся плитки, но ни одной свободной пары. Можно было бы реализовать backtracking, но повторный запуск на классической черепахе почти всегда успешен, а код остаётся читаемым. Шестидесяти попыток более чем достаточно.
def paste(order, pool):
pairs = random.sample(pool, len(order))
for k, (a, b) in enumerate(order):
tiles[a].face = pairs[k][0]
tiles[b].face = pairs[k][1]
Важно понимать: гарантия проходимости не означает, что игрок не может проиграть. Она гарантирует, что раздача проходима, но игрок может сделать неправильный ход и зайти в тупик — это часть игры. Поэтому тот же алгоритм запускается по требованию: когда игрок застревает, он может перемешать оставшиеся плитки, и алгоритм гарантирует, что новое расположение снова будет проходимым.
idxs = [i for i, tl in enumerate(tiles) if not tl.gone]
order = removal_order(idxs)
if order:
paste(order, pool_from_tiles(live_tiles()))
Таким образом, игрок никогда не оказывается в безвыходной ситуации. Вместо экрана «Game Over» появляется сообщение и кнопка «Перемешать».
Если вы разрабатываете игру Mahjong Solitaire или любую другую игру, где случайная раздача может привести к нерешаемым состояниям, используйте обратную раздачу. Это просто реализовать, и это значительно улучшает пользовательский опыт. Попробуйте написать свой генератор на Python уже сегодня!
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →